前面學習了進程,現在來看看線程。進程可以說是一個正在運行的程序的實例,其實它只是一個運行的程序的一個運行環境,可以說是一個監控者,它負責程序的初始化, 運行期的流程控制,結束時的一些清除工作。而執行程序真正的工作者是線程。現在就來介紹下線程。
線程和進程的組成非常相似,由下面兩部分組成
- 線程內核對象。線程內核對象和進程內核對象相似,是用來存儲線程的一些統計信息的簡單的數據結構
- 線程的堆棧。它用於維護所有線程執行時所用到的參數和局部變量
plus:
- 每個線程必須有一個進入點函數,線程是從進入點函數開始運行。
- 當進入點函數返回時,線程終止運行。堆棧被釋放,線程內核對象計數遞減1。
- 主線程的進入點函數必須爲main,wmain,WinMain,wWinMain之一,而一般線程函數可以用任何名字,但是,注意如果這些函數名要不同,不然系統會認爲是某個線程函數的不同實現而已
- 線程函數必須返回一個值,它將成爲該線程函數的退出代碼
- 儘量多的使用線程函數的參數和局部變量,因爲這些都是在線程堆棧中創建的,它不會被其他線程破壞
下面來大概的說下CreateThread函數
原形:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to thread security attributes
DWORD dwStackSize, // initial thread stack size, in bytes
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function
LPVOID lpParameter, // argument for new thread
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId // pointer to returned thread identifier
)
參數:
- lpThreadAttribute:是指向S E C U R I T Y _ AT T R I B U T E S結構的指針。如果想要該線程內核對象的默認安全屬性,可以(並且通常能夠)傳遞N U L L。
- dwStackSize:用於設定線程可以將多少地址空間用於它自己的堆棧。這裏說下/stack:[reserve][,commit]。這個開關同樣可以告訴線程怎麼被分配內存。reserve:爲這個堆棧保留的大小(默認1MB);commite:爲這個堆棧最初分配內存的大小(默認一頁)。當dwStackSize不爲0的時候,reserve是取dwStackSize和/stack:[reserve]中大的,而commit取/stack:[commit]。當dwStacjSize爲0時,就全部去/stack的值
- lpStartAddress AND lpParameter:這兩個值是線程函數的地址和線程函數的參數
- dwCreationFlag:可以設定用於控制創建線程的其他標誌。它可以是兩個值中的一如果該值是0,那麼線程創建後可以立即進行調度。如果該值是C R E AT E _ S U S P E N D E D,系統可以完整地創建線程並對它進行初始化,但是要暫停該線程的運行,這樣它就無法進行調度。
- lpThreadid:是新線程的的ID的存放地址,必須是DWORD的地址。
說完了創建線程,現在來說下終止線程,有四個方法:
- 線程函數返回
- 自己的進程的線程調用ExitThread
- 其他進程或者自己進程的線程調用TerminateThread
- 進程終止
她們當中最好的辦法就是:線程函數返回,因爲這樣可以保證:
- 在線程中創建的c++對象會被調用析構函數,正確清楚
- 操作系統能正確釋放線程堆棧佔用的內存
- 系統將退出代碼設置爲函數退出的返回值
- 線程將遞減內核對象1
如同進程退出的4個方法一樣,調用ExitThread,雖然系統能夠保證線程佔用的空間能正確清楚,但是c/c++ runtime得不到清除。而TerminateThread則更加的不好,因爲它是一個異步函數,調用不能確保線程馬上被清除。
好了,說到這裏線程的創建,刪除都說完了。。但是好象還缺點什麼。進程中有一個啓動函數,完成了進程的初始化,然後再調用程序員編寫的進入點函數。那麼線程當然也有自己的初始化環節,下面就來說下線程的初始化。先來看看這個圖:
初始化過程:
- 當程序調用CreateThread,系統就創建內核對象,使它的計數爲2(爲什麼爲2呢?因爲這個線程內核對象的句柄其實有兩個,一個是新線程中有一個自己的HANDLE,然後就是調用CreateThread函數的線程中有一個HANDLE,所以要釋放線程內核對象必須要將新線程停止運行並且把CreateThread返回的HANDLE關閉)。
- 暫停計數被設置爲1,退出碼始終爲S T I L L _ A C T I V E(0 x 1 0 3),該對象設置爲未通知狀態。
- 內核對象創建完成,現在就創建線程堆棧。線程堆棧的空間是從進程的地址空間分配而來的
- 系統將pvParam和pfnStartAddr寫入線程堆棧。
- 每個線程都有它自己的一組C P U寄存器,稱爲線程的上下文。該上下文反映了線程上次運行時該線程的C P U寄存器的狀態。線程的這組C P U寄存器保存在一個C O N T E X T結構中。C O N T E X T結構本身則包含在線程的內核對象中。
下面再來說下c/c++ runtime library對線程的考慮。單線程應用程序和多線程應用程序其實是調用不同的c/c++ runtime的。其實一共有6個c/c++ runtime library,他們是:
Library Name | Description |
---|---|
LibC.lib | Statically linked library for single-threaded applications. (This is the default library when you create a new project.) |
LibCD.lib | Statically linked debug version of the library for single-threaded applications. |
LibCMt.lib | Statically linked release version of the library for multithreaded applications. |
LibCMtD.lib | Statically linked debug version of the library for multithreaded applications. |
MSVCRt.lib | Import library for dynamically linking the release version of the MSVCRt.dll library. This library supports both single-threaded and multithreaded applications. |
MSVCRtD.lib | Import library for dynamically linking the debug version of the MSVCRtD.dll library. The library supports both single-threaded and multithreaded applications. |
爲什麼會有不同版本的c/c++ rumtime呢?原因是最早的c/c++ runtime是在1970年開發出來的。那個時候的程序都是單線程,沒有考慮到多線程,所以運行多線程應用程序會有問題。其實多線程c/c++ runtime運行程序時需要一個數據結構,這個數據結構與每個線程像聯繫,記錄了程序運行時c/c++ runtime的狀態,這樣就能保證在多線程運行時,保持c/c++ rumtime的同步性。然後就是,創建線程不會直接調用操作系統的CreateThread,而是調用_beginthreadex,其實這兩個函數的原形差不多,所以,只要設置一個函數的宏就可以讓你在編寫多線程應用程序時和單線程一樣了。而_beginthreadex完成了些什麼功能呢?下面就來一一例舉:
- 在函數裏給每個創建的線程分配一個c/c++ runetime堆棧分配的tiddata內存結構
- 傳遞給_beginthreadex的線程函數的地址以及參數保存在tiddata內存塊中
- _beginthreadex從內部調用CreateThread
- 當調用CreateThread時,其實是通過調用_threadstartex而不是線程函數來啓動線程的。還有傳遞給-threadstartex是tiddata而不是線程函數的參數
- 如果成功就返回線程句柄,失敗返回NULL
這裏說到了一個_threadstartex,那它又做了些什麼呢?
- 新線程從BasethreadStart執行,然後轉移到_threadstartex
- tiddata被傳遞給_threadstartex
- 操作系統通過調用TlsSetValue將tiddata與該線程聯繫起來
- 一個SEH偵被放置在線程周圍
- 調用線程函數,並傳遞參數
- 線程函數的返回值被認爲是線程的退出代碼,並且調用_endthreadex(注意,並沒有直接返回到BaseThreadStart,這樣會導致tiddata沒有釋放,從而內存泄露)
那麼_endthreadex又做些什麼呢?
- 該函數調用_getptd得到與此想對應的tiddata地址
- 然後該數據塊被釋放,再調用ExitThread
plus:前面說了,直接調用ExitThread會導致所有創建的c++對象不能撤消,現在再加一條,會導致tiddata不能被釋放。還有就是多線程c/c++ runtime能使malloc函數同步,不會出現兩個線程同時申請某塊內存的事情發生