熟練掌握Windows下的多線程編程,可以讓咱們編寫出更規範多線程代碼,避免不要的異常。Windows下的多線程編程很是複雜,可是瞭解一些經常使用的特性,已經可以知足咱們普通多線程對性能及其餘要求。編程
進程就是正在運行的程序。主要包括兩部分:windows
• 一個是操做系統用來管理進程的內核對象。內核對象也是系統用來存放關於進程的統計信息的地方。瀏覽器
• 另外一個是地址空間,它包含全部可執行模塊或 D L L模塊的代碼和數據。它還包含動態內安全
線程就是描述進程的一條執行路徑,進程內代碼的一條執行路徑。一個進程至少有一個主線程,且能夠有多個線程。線程共享進程的全部資源。線程主要包括兩部分:數據結構
• 一個是線程的內核對象,操做系統用它來對線程實施管理。內核對象也是系統用來存放多線程
線程統計信息的地方。併發
• 另外一個是線程堆棧,它用於維護線程在執行代碼時須要的全部函數參數和局部變量。異步
進程使用更多的系統資源,由於每一個進程須要獨立的地址空間。而線程只有一個內核對象及一個堆棧。若是有空間資源和運行效率上的考慮,則優先使用多線程。正由於每一個地址有自已獨立的進程空間,因此每一個進程都是獨立互不影響的。而一個進程中全部線程是共用進程的地址空間的,這樣一個線程出問題可能影響到全部線程。像多標籤瀏覽器容易一個見面假死致使整個瀏覽沒法使用。因此像360瀏覽器等每一個標籤頁都是一個進程,這樣一個標籤頁面出問題並不會影響到其餘標籤頁面。函數
32位windows中,0~4G線性內存空間。0~2G爲應用程序內存空間(處於其中每一個進程都有獨立的內存空間),2G~4G爲系統內核空間(內核進程徹底共享)。那麼進程的最大可用內存就是2G,每一個線程棧的默認大小是1MB,理論上最多建立2048個線程,實際進程中還有一些其餘地方佔用內存,因此通常狀況下可建立的線程總數爲2000個左右。固然,若是想建立更多線程,能夠縮小線程的棧大小。性能
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
• lpThreadAttributes,描述線程安全的結構體,默認傳NULL.
• dwStackSize,堆棧大小,默認1MB.
• lpStartAddress,線程函數入口地址。
• lpParameter,線程函數參數。
• dwCreationFlags,線程建立時的狀態,0表示線程建立以後當即運行。CREATE_SUSPENDED表示線程建立完掛起,直到調用ResumeThread才運行。
• lpThreadId,指向1個變量接受線程ID,可爲NULL。
void ExitThread(DWORD dwExitCode);
函數將強制終止線程的運行,並致使損傷系統清除該線程所使用的全部操做系統資源。可是C++對象可能因爲析構函數沒有正常調用致使資源不能獲得正確釋放。附加的退出碼,能夠用GetExitCodeThread()函數能夠獲取。不建議使用此線程終止函數,由於可能致使資源沒有正確的釋放,通常都讓線程正常退出。另外,即使要強制終止線程,也要使用_endThreadEx(不使用_endThread),由於它兼顧了多線程資源安全。
BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);
該函數也是強制退出線程的,只不過此函數是異步的,即它告訴系統去終止指定線程,可是不能保證函數返回時線程已經被終止了。所以調用者必須使用WaitForSingleObject函數來肯定線程是否終止。所以此函數調用後終止的線程堆棧資源不會獲得釋放。通常不建議使用此函數。
對線程安全沒有一個比較具體的說明,簡單來講線程函數的操做是安全的。這裏的操做對象主要爲:變量、函數、類對象。
這裏的變量指非自定義類型的全局變量/靜態變量,或者經過線程參數傳入的變量。
•全部線程只讀取該變量,那麼該變量確定線程安全的。
•有1個線程寫操做該變量,其餘線程讀取該變量。這時就須要考慮volatile。當一段線程代碼屢次讀取變量的值時,編譯器默認會優化代碼只第1次會從內存上讀取值,其餘時候直接是從寄存器上讀取的。這樣若是其餘線程更新了變量的值,讀取的線程可能依然是從寄存器上讀取的。這個時候就須要告訴編譯器該變量不要優化,永遠是從內存上讀取。效率可能低一點,可是保證線程中變量的安全更重要。
•有多個線程同時寫操做該變量,那麼就必須考慮臨界區讀寫鎖等方法。
多線程出現以前就已經有C/C++運行時庫,因此C/C++運行時庫不必定是線程安全的。例如GetLastError()獲取的就是一個全局的變量值,針對多線程可能就會出錯。針對這個問題,MS提供了C/C++多線程運行時庫,而且須要配合相應的多線程建立函數。
•_beginthreadex
不建議使用_beginthread,由於它是早期不成熟的函數,由於它建立完成線程以後當即結束了句柄,致使不能有效控制線程。C/C++運行時庫函數_beginthreadex是對操做系統函數CreateThread的封裝,而且這裏使用了線程局存儲(TLS)來保證每一個線程都有自已的單獨的一些共用變量,例如像GetLastError()使用的變量。這樣每一個線程就可以保證全部的API函數都是線程安全的。
•AfxBeginThread
若是當前代碼環境是基於MFC庫的,那麼多線程建立函數必須使用MFC庫函數AfxBeginThread。這是由於MFC庫是對C/C++運行庫的再封裝,一樣會面臨MFC庫自己存在的一些線程不安全變量的操做。AfxBeginThread實際上是對_beginthreadex函數的再封裝,在調用_beginthreadex以前完成一些安全載入MFC DLL庫的的操做。這樣基於MFC的庫函數的調用纔是安全的。
除了C/C++運行時庫、MFC庫由於已經有處理線程安全外,其餘第三方庫,甚至包括STL都不是線程安全的。這些自定義的類庫,都須要自已去考慮線程安全。 這裏能夠利用鎖、同步及異步等內核對象來解決,固然也可使用TLS來解決。
在線程內核對象的內部有一個值,用於指明線程的暫停計數。當調用CreateThread函數時,就建立了線程的內核對象,而且內核對象裏的暫停計數被初始化爲 1,這樣操做系統就不會再分配時間片給線程。當建立的線程指定CREATE_SUSPENED標誌時,那麼線程就處於暫停狀,這個時候能夠給線程進行一些優先級設置等其餘初始化。當初始化完成以後,能夠調用ResumeThread來恢復。單個線程能夠暫時屢次,若是暫停了3次,則須要ResumeThread恢復3次才能從新讓線程得到時間片。
除了建立線程指定CREATE_SUSPENED來暫停線程外,還能夠調用SuspendThread來暫時線程。調用SuspendThread時,由於不知道當前線程正在作什麼,若是是正在進行內存分配或者正在一個鎖操做當中,可能致使其餘線程鎖死之類的。因此使用SuspendThread時必定要增強措施來避免可能出現的問題。
運行 Windows 的計算機中的處理器有兩個不一樣模式:「用戶模式」和「內核模式」。根據處理器上運行的代碼的類型,處理器在兩個模式之間切換。應用程序在用戶模式下運行,核心操做系統組件在內核模式下運行。多個驅動程序在內核模式下運行,但某些驅動程序在用戶模式下運行。
當啓動用戶模式的應用程序時,Windows 會爲該應用程序建立「進程」。進程爲應用程序提供專用的「虛擬地址空間」和專用的「句柄表格」。因爲應用程序的虛擬地址空間爲專用空間,一個應用程序沒法更改屬於其餘應用程序的數據。每一個應用程序都孤立運行,若是一個應用程序損壞,則損壞會限制到該應用程序。其餘應用程序和操做系統不會受該損壞的影響。
用戶模式應用程序的虛擬地址空間除了爲專用空間之外,還會受到限制。在用戶模式下運行的處理器沒法訪問爲該操做系統保留的虛擬地址。限制用戶模式應用程序的虛擬地址空間可防止應用程序更改而且可能損壞關鍵的操做系統數據。
實現操做系統的一些底層服務,好比線程調度,多處理器的同步,中斷/異常處理等。
顧名思義,內核對象即內核建立的對象。因爲內核對象的數據結構只能被內核訪問,因此應用程序沒法在內存中找到這些數據內容。由於要用內核來建立對象,因此必從用戶模式切換到內核模式,而從用戶模式切換到內核模式是須要耗費幾百個時鐘 週期的。建和操做若干類型的內核對象,好比存取符號對象、事件對象、文件對象、文件映射對象、I / O完成端口對象、做業對象、信箱對象、互斥對象、管道對象、進程對象、信標對象、線程對象和等待計時器對象等。內核對象是跨進程的,因此跨進程可使用內核對象進行通訊。
早期CPU是單核單線程,因此不可能作到真正的多線程。時間片便是操做將CPU運行的時間劃分紅長短基本一致的時間區,便是時間片。多線程主要是經過操做系統不停地切換時間給不一樣的線程,來讓線程快速交替運行,由於時間相隔很短,用戶看起來像是幾個線程同時在運行。固然如今CPU有多核多線程,能夠作到真正的多線程了。可使用SetThreadAffinityMask來指定線程運行在不一樣CPU上。
sleep(0),當1個線程有大量計算量,容易致使CPU使用很高,而其餘進程線程得不到時間片。這個時候調用sleep(0),至關告訴操做系統從新來分配時間片,這個時候同優先級的線程就可能分配得時間片,減緩計算線程大量佔用時間片。
線程同步問題在很大程度上與原子訪問有關,所謂原子訪問,是指線程在訪問資源時可以確保全部其餘線程都不在同一時間內訪問相同的資源。
例如:
int g_nVal = 0;
DWORD WINAPI ThreadFun1(PLOVE pParam)
{
g_nVal++;
return 0;
}
DWORD WINAPI ThreadFun2(PLOVE pParam)
{
g_nVal++;
return 0;
}
由於g_nVal++是先從內存上取值放寄存器上再來進行計算,由於線程調度的不可控性,致使可能兩個線程前後都是從內存上取到的0,這樣自加後的結果都是1。這與咱們實際想要的結果2並不一致。爲了不這種狀況,就須要原子操做InterlockedExchangeAdd(g_nVal, 1)來達到效果。互鎖函數操做一個內存地址時,會防止另外一個CPU訪問內一個內存地址。
InterlockedExchanged/InterlockedExchangePointer,前者是交換一個值,後者是交換一組值。其做用是原子交換指定的值,並返回原來的值。所以它能夠有以下的應用。
void Fun()
{
while (InterlockedExchange(&g_bVal, TRUE) == TRUE)
Sleep(0);
// do something
InterlockedExchange(&g_bVal, FALSE);
}
上面的代碼可以達到一個鎖的效果。原子操做不用切換到內核模式,因此速度比較快。可是上面的代碼依然須要不停地循環來達到等待的效果。臨界區與原子操做同樣,均可以直接在用戶模式下操做,而且臨界區則是直接等待徹底不用給當前線程分配CPU時間片。因此效率上仍是臨界區更優一點。
當線程頻繁建立時,大量線程的建立銷燬會佔用大量的資源,致使效率低下。這個時候就能夠考慮使用線程池。線程池的主要原理,即建立的線程暫時不銷燬,加入空閒線程列表。當須要建立新線程時,優先去空閒線程列表中查詢是否有空閒線程,有就直接用,若是沒有再建立新的線程。這樣就可以達到減小線程的頻繁建立與銷燬。
像Python、Lua都提供了協程,尤爲是Lua,由於它沒有多線程,因此很是依賴協程,Lua也是將協程發揮得比較好的腳本語言。像其餘語言也都有第三方實現的協程庫可用。Windows多線程是由內核提供的,因此建立多線程須要切換到內核模式,由於從用戶模式切換到內核模式分花費幾百個時鐘週期。而一種直接由用戶模式提供的輕量級類多線程,其實就是協程(Coroutine)。具體來說就是函數A調用協程函數B,而後B執行到第5行中斷返回函數A繼續執行其餘函數C,而後下次再次調用到B時,這個時候是從B函數的第5行開始執行的。看起來就是先執行協程函數B,執行了一部分,中斷去執行C,執行完C接着從上次的位置執行B。看起來是簡陋的多線程,實際上是利用同步達到異步的效果。C++的主要實現原理,是經過保存函數的寄存器上下文以及堆棧,下次執行協程函數時,首先恢復寄存器上下文以及堆棧,而後跳轉到上次執行的函數。若是有大規模的併發,不但願頻繁調用多線程,能夠考慮使用協程。