在Windows系統中,進程更像一個容器,其功能的實現是靠線程完成的,即進程具有惰性。一個進程,至少擁有一個線程來執行任務。(進程第一個被創建的線程叫做主線程,其他的進程中的線程均為其子線程)
## 線程數據結構
對進程來說,一個進程包括 一個地址空間 和 一個內核對象。
對于線程,類似的 擁有一個內核對象 和 一個線程棧(用于維護線程執行時 所需要的函數參數及變量)。
同時,對于線程來說,他們還有一個進程層面的存儲空間TLS(線程本地存儲),這部分存儲空間用于存儲線程的私有全局變量用于支持非線程安全的C/C++函數。
## 進程與線程的資源開銷
進程擁有獨立的地址空間,線程在進程內共享進程空間,因此線程間可以直接處理相同的數據及共享句柄對象(句柄表是屬于進程的,可有線程共享)。
操作系統對進程切換的處理需要有大量的記錄(虛擬空間的切換),以及exe和dll加載入地址空間的消耗,因此進程的切換遠大于線程。
線程切換僅僅是線程上下文的切換(即運行時寄存器的少量記錄與轉換)。
## 線程的創建
線程的創建需要 一個線程函數(用于線程執行任務) 及 線程創建線程的函數。
Windows中線程函數原型如下
~~~
DWORD WINAPI ThreadFunc(PVOID pvParam)
{
....
return 0;
}
~~~
對應的創建Windows線程的函數為
~~~
HANDLE WINAPI CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
~~~
但是在實際中,如果我們是編寫的C/C++代碼,那么我們不要使用這個函數,因為其并不完整的支持非線程安全的C/C++函數(可能會導致內存泄露)。這里,應該使用Microsoft C
/C++編譯器提供的_beginthreadex函數(或使用C++ 11標準中的thread類)
這里需要記住的是,一個線程調用CreateThread函數后,其返會的線程內核對象句柄(Handle)的引用計數是2,(創建該線程1的線程一個,線程自己一個,線程自己的會在線程函數執行完畢后自動close其句柄)。而內核對象銷毀的必要條件是句柄引用為0。為了防止資源泄露,在創建了線程后主動掉一下CloseHandle是一個不錯的習慣。比如
~~~
DOWD WINAPI ThreadFunc(PVOID pvParam)
{
....
return 0; // will close thread handle automatly
}
...
HANDLE hThread = CreateThread(.., ThreadFunc, ...);
CloseHandle(hThread); // Good!
~~~
## Windows線程的創建過程
當用戶調用函數創建線程后,windows系統會做兩件事:
1、創建一個代表新線程的內核對象。
2、在當前進程空間中分配內存空間給新線程的線程棧。
這里還要強調下,因為新線程是與其他線程在同一個進程地址空間中運行,因此新線程可以直接訪問進程的所有句柄,內存空間以及其他線程的線程棧。因此同一進程的線程通信非常簡單,但要注意線程間同步的問題。
## CreateThread函數參數意義
psa
線程安全屬性
*dwStackSize*
默認線程棧的大小(以字節為單位)。線程棧的大小同時可以在編譯器的/STACK指定,默認為1M。線程棧會取其中較大的一個值。限制線程棧的大小可以檢測到無窮遞歸的bug。
*lpStartAddress*和*lpParameter*
分別指向線程函數地址與線程參數地址。
*dwCreationFlags*
線程創建后執行標志。
若為0,則線程立即執行。
若為**CREATE_SUSPENDED**,則線程創建后不會立即執行,而是等待[**ResumeThread**](https://msdn.microsoft.com/en-us/library/ms685086%28v=vs.85%29.aspx)函數手動執行。
**STACK_SIZE_PARAM_IS_A_RESERVATION**若設置該flag,則線程棧空間只會預定而不會實際分配。若沒有設置,則默認直接分配線程棧空間。
*lpThreadId* 傳出函數,獲取新建線程的id。傳入null則不關心該thread id。
## 終止線程的運行
如果我們編寫的是C++的windows程序,那么,在線程終止時,我們應該期待如下四點的清理工作能夠執行:
1、屬于該線程棧上的所有C++對象的析構函數得以調用。(C++要求)
2、該線程的線程棧使用的內存能夠正確的釋放。(windows要求)
3、線程內核對象引用計數減一(注意這里是減一而不是銷毀。線程內核對象生命可能會長于線程函數本身)。(windows要求)
4、操作系統將線程退出代碼作為線程函數的返回值。(windows要求)
基于上述4點要求,依次比較線程結束的四種方法。
### 線程函數結束
這是唯一能夠完全執行線程清理方法的結束方式。
### ExitThread函數
由被終止線程自身調用,可傳入線程返回代碼作為線程返回值。該函數屬于windows系統API,因此只會執行2——3的windows清理工作,而C++對象不會析構。
(注意:若使用的是C/C++代碼,必須得調用ExitThread函數時,應該使用_endthreadex函數代替,原因見后面)
### TerminateThread函數
該函數可以殺死任意線程(取決于傳入該函數的線程句柄)。應該知道:
該函數是異步的。
由于該函數的突然性,被殺死的線程資源不會得到清理(但內核對象計數會減一)。
數據不會寫回磁盤。
C++對象不會析構(由于是windows API函數)。
在dll的main函數中不會得到被TerminateThread函數殺死線程的退出通知。
### 包含線程的進程退出
這種情況下,類似于對每個進程內線程調用TerminateThread函數,但操作系統會保證進程的資源全部釋放(即內核對象、線程棧資源都會釋放 )。但C++對象不會得到析構,同時數據不會寫回到磁盤……
因此,在進程返回前,我們通常會調用join或wait函數等待其內線程函數的結束。
這里要注意的是dll main函數中多線程死鎖的問題,詳細可見:
http://blog.csdn.net/guke1978_123/article/details/625773
## 線程結束后
現在討論線程結束后,操作系統肯定會執行的操作。
1、線程所擁有的用戶對象句柄被釋放(窗口和掛鉤)
2、線程退出代碼由STILL_ACTIVE變為Exitthread或TerminateThread的傳入值。
3、線程內核對象變為觸發狀態。
4、線程內核對象計數減一。
5、若該線程為進程最后的活動線程,則進程終止。
注意,當線程結束時,其內核對象并不會立即銷毀,只有當說有擁有該線程內核對象的線程關閉其句柄時,句柄對象才會真正銷毀。因此,在線程結束時,其他線程仍可以調用
GetExitCodeThread函數獲取線程的退出代碼。