1. 程式人生 > >揭祕!為何要用_beginthreadex,而不用CreateThread和_beginthread

揭祕!為何要用_beginthreadex,而不用CreateThread和_beginthread

       由於歷史原因,所以C/C++執行庫並不是為多執行緒應用程式而設計的,所以為了保證其中的某些變數和函式的安全,那麼必須建立一個數據結構,並使之與使用了C/C++執行庫函式的每個執行緒所關聯。當在呼叫C/C++執行庫函式時,那些函式必須讀取主調自己的執行緒的資料塊,從而避免印象其他執行緒。

       所以當編寫C/C++程式碼時,請呼叫_beginthreadex,而不要使用CreateThread的大致原因就是如上所述。詳細內容請繼續往下讀大笑

       _beginthreadex的函式引數列表與CreateThread一樣,但是引數名和型別有所不同。這是因為C/C++程式不應該對Windows系統的型別有任何的依賴。

       _beginthreadex也會返回新建執行緒的控制代碼,就像CreateThread一樣。可以非常方便的替換自己程式中的CreateThread函式,但是資料型別不一樣,所以還得新增一些型別轉換即可。

       透過_beginthreadex函式的原始碼我們可知,

        1.每個執行緒都有自己專用的_tiddata記憶體塊,它們是從C/C++執行庫的堆上分配的。

        2.傳給_beginthreadex的執行緒函式地址(pfnStartAddr)儲存在_tiddata記憶體塊中。(_tiddata結構可以在Mtdll.h原始碼中窺探一二)

        3.

_beginthreadex會在內部呼叫CreateThread。

       4.呼叫CreateThread時傳入的地址是_threadstartex(而非傳入的pfnStartAddr)。引數地址是_tiddata結構地址,而非pvParam。

        5.如果一切順利,會返回執行緒的控制代碼,操作失敗會返回0。

      初始化並且傳入_tiddata後,又怎麼將其和執行緒關聯呢?繼續往下看大笑

      對於_threadstartex函式及其輔助函式__callthreadstartex:

        1.新的執行緒首先執行RtlUserThreadStart,然後在跳轉到_threadstartex。(這便於上一篇所講的"建立執行緒的內幕"一文中的知識點所契合)

        2._threadstartex唯一的引數就是新執行緒的_tiddata記憶體塊地址。

      3.TlsSetValue是一個作業系統函式,作用是將一個值與主調函式關聯起來。(這就是所謂的執行緒區域性儲存TLS)_threadstartex函式將_tiddata記憶體塊與新建執行緒關聯起來。------華麗的分割線--------

        4.無引數的輔助函式__callthreadstartex中,有一個SEH幀,它處理著許多與執行庫有關的事情——比如執行時錯誤(如丟擲未被捕獲的C++異常)——和c/c++執行庫的signal函式。這點很重要,因為用CreateThread建立的執行緒,呼叫C/C++執行庫的signal函式,其是不能正常工作的。

       5.預期要執行的執行緒函式會被呼叫,並向其傳遞預期的引數。(執行緒的地址和引數都是存在於TLS中儲存的_tiddata資料塊中,並會在__callthreadstartex中在TLS中獲取得到)6.執行緒函式的返回值被認為是執行緒的退出程式碼。注意:函式不是返回_threadstartex然後返回RltUserThreadStart(因為這樣_tiddata記憶體塊不會被釋放),而是呼叫_endthreadex,並向其傳入執行緒函式的返回值。

對於_endthreadex,其內部先獲取TLS中的_tiddata塊,然後釋放掉,並呼叫作業系統的ExitThread函式來實際銷燬執行緒,並傳遞正確的退出碼。

        總結:所以現在應該理解為什麼不要呼叫CreateThread了,因為呼叫CreateThread後,線上程呼叫一個需要_tiddata塊的c/c++執行庫函式時(主要通過TlsGetValue),會得到NULL,這時c/c++執行庫會為其初始化一個塊,然後這個塊與執行緒關聯(主要通過TlsSetValue)。這之後,任何使用_tiddata塊的函式都能正常使用了。但是,問題還是有的:1.如果執行緒使用了signal函式,則整個程序會終止掉(上文有詳細描述,關於__callthreadstartex的SEH幀沒有準備就緒)。2.執行緒入口點函式返回後,會在RtlUserThreadStart中直接呼叫ExitThread,其並沒有釋放掉_tiddata記憶體塊,這會引起記憶體洩漏。

        附加:絕不呼叫_beginthread。因為,1._beginthread引數較少,不能建立具有安全屬性的執行緒,不能讓執行緒立即掛起等。_endthread也是如此,其退出程式碼被硬編碼為0。

_endthread還存在一個問題,就是在呼叫ExitThread前,會呼叫CloseHandle,向其傳入新執行緒的控制代碼。舉個例子,比如你使用了_beginthread建立了一個新的執行緒,在其之後使用CloseHandle關閉執行緒控制代碼。由於_beginthread會呼叫_endthread函式終止執行緒,而執行緒核心物件的初始化計數為2,在_endthread函式中,呼叫ExitThread前,會呼叫CloseHandle,也就是說會連續兩次遞減引用計數,這時引用計數為0,核心物件被系統銷燬,再之後在呼叫CloseHandle關閉執行緒控制代碼時,這個傳入的控制代碼是無效的,即CloseHandle函式的呼叫會以失敗告終。所以請用_beginthreadex代替_beginthread,_endthreadex代替_endthread。      _beginthreadex呼叫的是_endthreadex,_beginthread呼叫的是_endthread