Friday, April 01, 2011

Win32程式設計: _beginthreadex or CreateThread?

先說結論,永遠先用_beginthreadex()來建立新的執行續,把它當作是你的 Code Convention。

為什麼?先從動機說起。

在 C Run-Time (CRT) 函式庫裡有許多函式在多執行續的環境下,會利用 Thread Local Storage (TLS) 去存取每個獨立執行續的資源,藉此達到 Thread-Safe。

舉例來說,errno 是檢查錯誤碼的變數,在多執行續的環境下,如果A()和B()兩個函式從不同的執行續改寫了 errno,那麼接下來的程式碼就無法確認 errno 來決定上面一個函式究竟是成功還是失敗。

為了解決這種問題,在多執行續的 CRT 下,errno 會被替換成 #define errno (*_errno())。_errno() 裡面做的事情就是去取出 TLS 裡面存放的 per-thread errno。這樣每一個執行續都有自己的 errno,不用擔心內容不正確的問題。

像是 errno 這樣的應用還有很多,它們都是存放在 _tiddata 這個結構中。_tiddata 又是放在 TLS 裡面。

再回來看_beginthreadex(),它比 CreateThread() 是多做了一些事情。最主要就是多初始一份 _tiddata 並且放在新的執行續的 TLS 中,讓 CRT 的函式可以使用這些 _tiddata 來達成 Thread-safe。

你可能會問,那...我的產品都已經釋出了,而且都是用CreateThread(),怎麼辦?先別擔心,先讓我們來看沒有正確使用 _beginthreadex() 會造成什麼影響。

  • _tiddata 沒有初始化?
    如果沒有初始化,在 CRT 的函式中只要是第一個使用到 _tiddata 的地方,還是會負責建立一份的。舉例來說,_errno() 裡面檢查到沒有 _tiddata,它會自己建立一份,再放回 TLS 中。所以你的 CRT 函式還是會正確運作的。
  • 記憶體洩漏?
    正常來說,_tiddata 只會在 _endthreadex() 呼叫之後才會被刪除掉。_beginthreadex() 產生出來的執行續在結束後會自動呼叫到 _endthreadex()。所以理論上 CreateThread() 產生的執行續就會有記憶體洩漏的問題。但是這點,Microsoft的工程師也幫你想到了,不管你是 Static-link 或是 Dynamic-link 多執行續的 CRT,CRT 會在收到 DLL_THREAD_DETACH 的時候再幫你檢查 _tiddata 然後把它刪除掉。

    不過這邊 Static-link 可能會有個問題。舉例來說,A.exe 裡面用了 errno 然後當下 malloc 了一份 _tiddata,然後 call-stack 進入到 B.dll,之後 B.dll 返回離開 call-stack 就會收到 DLL_THREAD_DETAH,B.dll 就會刪除這個 _tiddata,可是 A.exe 和 B.dll 是分別使用獨立的 Heap,這樣一刪除下去,恐怕會有 Heap Corruption 的機會。(純粹推理,還沒驗證過)
  • 還有其他的問題嗎?
    _beginthreadex() 還會呼叫 _fpclear() 和支援.NET AppDomain,前者和浮點數相關函數初始化有關,後者會將新的執行續在 Caller 相同的 AppDomain 上執行。
    (反組譯了一下我電腦上的 msvcr80.dll,_fpclear()其實什麼都沒做就返回了。)

如果是用 Multithread Dynamic-Link C Run-Time 看起來就沒什麼問題囉。(謎之聲:那個在 Production Code 用了 CreateThread 的人是誰呢? XD)


參考資料:

No comments: