多執行緒自增問題
i++
考慮變數i++的操作,實際上可以分解為以下3步:
(1)從記憶體單元讀入暫存器;
(2)在暫存器中對變數做增量操作;
(3)把新的值寫回記憶體單元。
如果兩個執行緒試圖幾乎在同一時間對同一變數做增量操作而不進行同步的話,結果可能就不一致了,在上述程式碼中,我們傳進執行緒函式的是變數的地址,那麼變數i自增後,可能還沒有寫回記憶體單元,就被另一個執行緒讀取了,那為什麼不是隻建立了一個執行緒了,而是確確實實建立了兩個執行緒了。
原子操作:Interlocked
現在模擬50個使用者登入,為了便於觀察結果,在程式中將50個使用者登入過程重複20次,程式碼如下:
-
#include <stdio.h>
- #include <windows.h>
- volatilelong g_nLoginCount; //登入次數
- unsigned int __stdcall Fun(void *pPM); //執行緒函式
- constDWORD THREAD_NUM = 50;//啟動執行緒數
- DWORD WINAPI ThreadFun(void *pPM)
- {
- Sleep(100); //some work should to do
- g_nLoginCount++; //重點重點重點重點重點重點重點
- Sleep(50);
- return 0;
- }
-
int main()
- {
- printf(" 原子操作 Interlocked系列函式的使用\n");
- printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
- //重複20次以便觀察多執行緒訪問同一資源時導致的衝突
- int num= 20;
- while (num--)
- {
- g_nLoginCount = 0;
- int i;
- HANDLE handle[THREAD_NUM];
-
for
- handle[i] = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL);
- WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
- printf("有%d個使用者登入後記錄結果是%d\n", THREAD_NUM, g_nLoginCount);
- }
- return 0;
- }
執行結果如下圖:
現在結果水落石出,明明有50個執行緒執行了g_nLoginCount++;操作,但結果輸出是不確定的,有可能為50,但也有可能小於50。
要解決這個問題,我們就分析下g_nLoginCount++;操作。在VC6.0編譯器對g_nLoginCount++;這一語句打個斷點,再按F5進入除錯狀態,然後按下Debug工具欄的Disassembly按鈕,這樣就出現了彙編程式碼視窗。可以發現在C/C++語言中一條簡單的自增語句其實是由三條彙編程式碼組成的,如下圖所示。
講解下這三條彙編意思:
第一條彙編將g_nLoginCount的值從記憶體中讀取到暫存器eax中。
第二條彙編將暫存器eax中的值與1相加,計算結果仍存入暫存器eax中。
第三條彙編將暫存器eax中的值寫回記憶體中。
這樣由於執行緒執行的併發性,很可能執行緒A執行到第二句時,執行緒B開始執行,執行緒B將原來的值又寫入暫存器eax中,這樣執行緒A所主要計算的值就被執行緒B修改了。這樣執行下來,結果是不可預知的——可能會出現50,可能小於50。
因此在多執行緒環境中對一個變數進行讀寫時,我們需要有一種方法能夠保證對一個值的遞增操作是原子操作——即不可打斷性,一個執行緒在執行原子操作時,其它執行緒必須等待它完成之後才能開始執行該原子操作。這種涉及到硬體的操作會不會很複雜了,幸運的是,Windows系統為我們提供了一些以Interlocked開頭的函式來完成這一任務(下文將這些函式稱為Interlocked系列函式)。
Interlocked系列函式
1.增減操作
LONG__cdeclInterlockedIncrement(LONG volatile* Addend);
LONG__cdeclInterlockedDecrement(LONG volatile* Addend);
返回變數執行增減操作之後的值。
LONG__cdec InterlockedExchangeAdd(LONG volatile* Addend, LONGValue);
返回運算後的值,注意!加個負數就是減。
2.賦值操作
LONG__cdeclInterlockedExchange(LONG volatile* Target, LONGValue);
Value就是新值,函式會返回原先的值。
在本例中只要使用InterlockedIncrement()函式就可以了。將執行緒函式程式碼改成:
- DWORD WINAPI ThreadFun(void *pPM)
- {
- Sleep(100);//some work should to do
- //g_nLoginCount++;
- InterlockedIncrement((LPLONG) & g_nLoginCount);
- Sleep(50);
- return 0;
- }
再次執行,可以發現結果會是唯一的,輸出都為50。
因此,在多執行緒環境下,我們對變數的自增自減這些簡單的語句也要慎重思考,防止多個執行緒導致的資料訪問出錯。