線程對于 Windows 編程人員來說,并不陌生,但是一直以來,我對它的了解也只是基本的使用層面。對于很多細節,也并不是很了解。這作為一個 Windows 客戶端開發人員,可以說是非常尷尬了。所以,抽了一點時間,仔細梳理了一下線程相關的內容。順便記錄下來。
一些常識
- 基本狀態:就緒,執行,阻塞
- 堆公有、棧私有
- 創建和結束所需要的系統開銷:小
- 沒有自己的地址空間
創建線程
在 Windows 下創建一個線程,很自然的會想到
CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ __drv_aliasesMem LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
這個方法可以說對 Windows 應用開發人員并不陌生。當使用這個方法的時候,在平時使用的時候,比較多關注的就是 lpStartAddress、lpParameter 。這是線程函數的入口以及參數。創建一個新線程之后,將會從這里開始執行。
但是對于 C++ 來說,其實有另一個方法
_ACRTIMP uintptr_t __cdecl _beginthreadex(
_In_opt_ void* _Security,
_In_ unsigned _StackSize,
_In_ _beginthreadex_proc_type _StartAddress,
_In_opt_ void* _ArgList,
_In_ unsigned _InitFlag,
_Out_opt_ unsigned* _ThrdAddr
);
在這里,_StartAddress、_ArgList 則跟上述那兩個參數是類似的作用。
然而在這兩個方法的選擇中,《Windows 核心編程》早有公斷。
根據作者的說法是選擇 _beginthreadex 替代 CreateThread 。而原因則要從 _beginthreadex 的實現上說起。
_beginthreadex 在 Windows 下的實現也是調用了 CreateThread ,畢竟在 Windows 系統中,只認這一種創建線程的方式。但是在這之前,它還會做一些額外工作。創建一個線程數據塊( tiddata ),然后將入口和參數都保存到數據塊中,最后還要把數據塊保存在 TLS 中。之后還要初始化一個 SEH 幀,用來處理運行時產生的錯誤。然后在線程結束之前,釋放掉 tiddata 。那這樣看,確實要比 CreateThread 多做一些事情。
話說回來,如果不做這些事情,當然就會有問題。比較直接的問題就是內存泄漏。原因是,如果使用 CreateThread 創建線程,當調用一些運行庫函數的時候,會檢查這個 tiddata 。如果發現沒有,則會自己搞出一個,而這個在線程結束的時候,就不會被正確釋放,就出現了內存泄漏。
類似 errno 這種運行庫函數,需要反應正確的錯誤信息,如果不記錄線程相關信息,則會在多線程的時候出現錯誤,所以一個 tiddata 是必要的,這也說明了為什么這個 tiddata 無論什么情況都會存在。
所以綜上所述,在創建線程是,應該選擇 _beginthreadex。
關于更詳細的 _beginthreadex 內容,參考 _beginthread, _beginthreadex 這篇文章是最好了
TLS
上邊說的 TLS。可謂是線程中不可缺少的東西。因為線程之間是共享地址空間的,所以當有一些每個線程自己所需要的數據的時候,就不那么方便。而 TLS 就是用來解決這個問題。存儲在 TLS 中的數據,對于每個線程之間,是互相隔離的。
結束線程
盡可能的讓線程執行完自然結束。不到萬不得已的時候,都不要使用 ExitThread 或者是 _endthreadex 。因為會使主調線程不正常返回,導致構造的 C++ 對象都不會析構;如果使用 ExitThread 還會造成 tiddata 不會被釋放。
后記
關于多線程編程其實坑不算少,唯有對 Thread 多一些了解,才能寫出更高質量的代碼。