使用多線程的時候往往需要數據之間的傳輸,例如子線程執行結束的時候要告訴主線程已經結束,又如,多個線程可能同時對某個全局的數據進行修改,雖然前面我們提到在線程中盡量使用局部變量和函數參數,但是在一些不可避免要使用全局變量並對其值進行修改的時候就要進行線程同步。
線程之間的通信很重要,尤其是在以下兩種情況下:
1:需要讓多個線程同時訪問一個共享資源,同時不能破壞資源的完整性。
2:一個線程需要通知其他線程某項任務已經完成。
WINDOWS提供了很多種線程同步的機制,下面介紹幾種用戶模式下的線程同步。
1:原子訪問
主要是使用Interlocked系列函數,Interlocked系列函數只能對單一的變量進行原子訪問,所謂原子訪問也就是一個線程在訪問的時候,系統會將其他訪問共享資源的線程進入等待狀態。
常用的Interlocked系列函數主要有
LONG InterlockedExchangeAdd( PLONG volatile plAddend, LONG lIncrement); LONGLONG InterlockedExchangeAdd64( PLONGLONG volatile pllAddend, LONGLONG llIncrement);
此函數區分不同的CPU平台,將plAddend指針所指向的內存數據進行增加llIncrement的原子操作,當然這個llIncrement是負值的話就是進行減操作了。
這個函數完全可以替代
InterlockedIncrement
這個函數其實就是將llIncrement設置為默認1。
下面是一些交換的原子操作
LONG InterlockedExchange( PLONG volatile plTarget, LONG lValue); LONGLONG InterlockedExchange64( PLONGLONG volatile plTarget, LONGLONG lValue); PVOID InterlockedPointer( PVOID *volatile ppvTarget, PVOID pvValue);
注意第三個函數,由於要交換的是指針,所以這裡ppvTarget是二級指針。
這組函數式帶有返回值的,返回值是原來ppvTarget裡面存放的數據。這就能保證在ppvTarget中的數據被重置了之後而不丟失原來的數據,在實現旋轉鎖上有著很好的作用。
Bool use=false; Void func() { While(InterlockedExchange(&use,true)==true) Sleep(0); //........ InterlockedExchange(&use,false); }
下面是實現旋轉鎖的代碼,這裡比較use也就是共享資源是否被訪問標志,首先將這個值設置為true,然後比較原來的值是否為true,也就是資源原來是否被占用,如果被占用就讓當前線程放棄該時間片的調度,如果資源原來沒有被占用就可以使用資源,最後在使用完共享資源後要重新將共享資源是否被訪問標志重新置為flase表示資源現在已經沒有被占用了。
需要說明的是在單CPU的系統上使用循環鎖是沒有意義的,只有在多CPU系統上才有用,當資源在一個CPU上被線程使用時,其他線程可以在其他的CPU上進行等待,多CPU的多線程讀取寫入處理要比單CPU的要慢,因為多CPU之間要進行高速緩存上的數據的同步,volatile關鍵字表示這個變量可能會被應用程序之外的其他東西修改,告訴編譯器不要對此變量進行優化。
使用Interlocked系列函數速度快,但是只能處理單一的數據,對於有很多數據需要同步的問題就無能為力了,這裡就要介紹第二種用戶模式同步機制。
2.使用關建段
關鍵段是一小段代碼,它在執行之前需要獨占一些共享資源的訪問權限,核心編程中把這個共享資源比喻成廁所裡面的馬桶,一個廁所同時只允許一個人進入使用馬桶,當這個人進入後就要把廁所的使用狀態改成正在使用,這樣其他的人這時就不能進入廁所使用馬桶了,在這個人離開廁所的時候需要把廁所的使用狀態重新改成無人使用,以方便其他的人進行使用。這個比喻很形象,這個的人就是線程,這裡要使用廁所之前需要進行初始化的操作,文中沒有進行比喻,我們就姑且比喻成在使用廁所的馬桶之前,你要脫掉褲子,也就是進行初始化的工作
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
在廁所安裝好了馬桶之後我們就可以上廁所了
Int g_a=0; CRITICAL_SECTION cs; DWORD WINAPI ThreadProc1(PVOID) { EnterCriticalSection(&g_a); for(int i=0;i<100;i++) g_a++; LeaveCriticalSection(&cs); return 0; } DWORD WINAPI ThreadProc2(PVOID) { EnterCriticalSection(&g_a); for(int i=0;i<100;i++) g_a++; LeaveCriticalSection(&cs); return 0; }
這裡的共享資源g_a也就是我們的馬桶了,當然這之前馬桶肯定是初始化好了的,只是通過Enter和Leave就能獨占的使用資源了。當我們在離開廁所的時候需要進行清理工作,這裡姑且比喻成上完廁所之後穿上褲子。
這種方式不是很好,因為當我們發現廁所被占用了之後就一直在那傻等著,這裡我們想象成我們應該找個小秘或者僕人之類的幫我們去看看現在能不能上廁所,這裡用到了TryEnterCriticalSection函數,這個函數就是我們的僕人了,該函數會返回true或者false表示現在廁所裡的馬桶是否被占用,這個僕人會幫我們一直等在廁所,而我們主線程)可以干一些其他的事情。
3.讀寫鎖
讀和寫操作不同在於讀可以同時進行,而寫不行,這裡讀寫鎖是對這種差異進行區別處理。
和關鍵段相似,使用讀寫鎖之前也要進行初始化操作,這裡可以把讀操作所需的資源比喻成廁所裡的水龍頭,把寫操作比喻成馬桶,在同一時間,我們可以幾個人同時洗手,但是不能幾個人同時解手。這個應該還是符合常理的。
首先還是初始化,蹲馬桶的就脫褲子,洗手的就把手拿出來。
Void InitializeSRWLock(PSRWLOCK SRWLock);
之後該干什麼干什麼了
先說蹲馬桶的吧
Void AcquireSRWLockExclusive(PSRWLOCK SRWLock);
還是,用完了記得提褲子等一些清理操作
Void ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
再說洗手
Void AcquireSRWLockShared(PSRWLOCK SRWLock); Void ReleaseSRWLockShared(PSRWLOCK SRWLock);
形式是驚人的相似啊,就是最後這個一個是可以共享的資源,一個是必須獨占的資源。
在其他人等待使用廁所的時候,我們一般是不希望一直等在那裡的,我們希望別人告訴我們廁所有沒有人,水龍頭或者馬桶能不能用,因為我們這個時候想做一些其他的事情,這裡就特指休息了,Windows提供SleepConditionVariableCS或SleepConditionVariableSRW函數,等待條件變量。線程在等待該條件變量時,會以原子方式把鎖釋放並將自己阻塞,直到該條件變量被觸發時為止。
Bool SleepConditionVariableCS( PCONDITION_VARIABLE pConditionVariable, PCRITICAL_SECTION pCriticalSection, DWORD dwMilliseconds); Bool SleepConditionVariableSRW( PCONDITION_VARIABLE pConditionVariable, PSRWLOCK pSRWLock, DWORD dwMilliseconds ULONG Flags);
當另一個線程檢測到相應的條件已經滿足時,比如存在一個元素可以讓讀取者線程讀取。它會調用WakeConditionVariable或WakeAllConditionVariable,觸發條件變量。這樣調用Sleep函數而阻塞在該條件變量的線程就會被喚醒。
Void WakeConditonVariable( PCONDITION_VARIABLE ConditionVariable); Void WakeAllConditionVariable( PCONDITION_VARIABLE ConditionVariable);
dwMilliseconds表示我們希望等待的時間,超過這個時間即使廁所沒人用我們也不用了,
Flag表示我們將以何種方式獲得鎖,這裡有CONDITION_VARIABLE_LOCKMODE_SHARED共享的方式,這裡傳0,表示獨占的方式。
本文出自 “賣萌程序員” 博客,請務必保留此出處http://7677869.blog.51cto.com/7667869/1302886