一、分析上一篇程序的現象
我們先從上一篇文章中的最后一個程序開始分析。
#include <stdio.h>
#include <windows.h>
const unsigned int THREAD_NUM = 10;
DWORD WINAPI ThreadFunc(LPVOID);
int main()
{
printf("我是主線程, pid = %d\n", GetCurrentThreadId()); //輸出主線程pid
HANDLE hThread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++)
{
hThread[i] = CreateThread(NULL, 0, ThreadFunc, &i, 0, NULL); // 創建線程
}
WaitForMultipleObjects(THREAD_NUM,hThread,true, INFINITE); //一直等待,知道所有子線程全部返回
return 0;
}
DWORD WINAPI ThreadFunc(LPVOID p)
{
int n = *(int*)p;
Sleep(1000*n); //第 n 個線程睡眠 n 秒
printf("我是, pid = %d 的子線程\n", GetCurrentThreadId()); //輸出子線程pid
printf(" pid = %d 的子線程退出\n\n", GetCurrentThreadId()); //延時10s后輸出
return 0;
}
看程序的輸出:
按照正常情況來看應該是每一行輸出兩列,但是中間有一行多出了一列,看圖中圈出來的地方,pid = 208 的線程輸出線程pid后並沒有馬上退出,而是等到了最后才退出。(可能每次運行的情況不一樣,這里只說明這一種情況),這是為什么的。 這里涉及到了線程調度的問題, 說明pid = 208 的線程輸出線程pid后操作系統進行了線程調度,cpu資源被其它線程搶占,這個線程直到最后才又重新分配到cpu資源,重新往下執行。
二、原子操作
這里明明是要寫原子操作,但是到目前為止,並沒有任何地方提及什么是原子操作,不要着急,接下來就慢慢來說。那么什么是原子操作呢?一個操作如果能夠不受中斷地完成,我們稱之為原子操作
我們來看這個程序
#include <stdio.h>
#include <windows.h>
const unsigned int THREAD_NUM = 50;
unsigned int g_Count = 0;
DWORD WINAPI ThreadFunc(LPVOID);
int main()
{
HANDLE hThread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++)
{
hThread[i] = CreateThread(NULL, 0, ThreadFunc, 0, 0, NULL); // 創建線程
}
WaitForMultipleObjects(THREAD_NUM, hThread, true, INFINITE); //一直等待,直到所有子線程全部返回
printf(" 總共 %d 個線程給 g_Count 的值加一,現在 g_Count = %d\n", THREAD_NUM, g_Count);
return 0;
}
DWORD WINAPI ThreadFunc(LPVOID p)
{
Sleep(50);
g_Count++;
Sleep(50);
return 0;
}
有一個全局變量 g_Count ,每個線程給這個全局變量加一,照這么來看最后應該輸出 50 ,我們看一下程序的輸出(每次都可能不一樣的結果)
為什么會這樣呢??? 明明有 50 個線程都給 g_Count 加一了,為什么輸出 46,根源在於 g_Count++; 這條語句上,這里就只有一條c++語句,按理說不應該有問題,其實不然,現在,在這里打下斷點,開始調試,打開反匯編窗口(Vs編譯器快捷鍵 Alt+8),如下圖
可以看到,這一條c++語句,被分成了三條匯編語句,先是把 g_Count 的值給寄存器 eax,然后寄存器 eax 的值加一,再把 eax 的值給 g_Count ,這樣就完成一次 g_Count++ 操作。出問題的原因就在於,在這幾條匯編語句執行的過程中發送了線程切換,比如,A線程剛執行完 add eax,1 還沒有把 eax的值給 g_Count,這時B線程開始執行,把 g_Count 原先的值又存入 eax,這就修改了 eax 中A線程計算好的值。
因此在多線程環境中對一個變量進行讀寫時,我們需要有一種方法能夠保證對一個值的遞增操作是原子操作——即這個操作不可以被打斷性,一個線程在執行原子操作時,其它線程必須等待它完成之后才能開始執行該原子操作。Windows系統為我們提供了一些以Interlocked開頭的函數來完成這一任務。這里只是介紹原子操作的概念,這和線程同步息息相關,但是這些以 以Interlocked 開頭的函數我們基本不用,就不一一介紹了,感興趣的可以自己去了解。