該用 _beginthread 還是 CreateThread?
_beginthread 為 MSVC CRT 的函式,而 CreateThread 為 Windows API。常聽到有人說在 Windows 上寫 application,最好使用 _beginthread 而不要用 CreateThread,那這是為什麼呢?
在說明之前先來講講 thread local storage (TLS) 是什麼東西。一般假使我們在一個 process 中有一個 global variable,而當我們產生了多個 thread 時,這些 thread 之間共用這一個 global variable。這樣會產生問題,假使我們未對這個 global variable 做保護,那麼因為 context switch 的關係,thread 之間讀寫這個變數可能會出問題。像 CRT 中的 errno,原本它只是一個變數,存放錯誤碼。但因為 multi-thread 的關係,一個 thread 寫入 errno 後,可能在未讀出來之前就被另一個 thread 修改掉了。
而 TLS 顧名思義,就是代表 thread 自已擁有的一塊空間。當我們產生新的 thread 時,每個 thread 都會有一個 TLS 變數的副本。在 MSVC 中,我們可以宣告 __declspec(thread) 來定義一個 TLS 的全域變數。而 Linux 中是使用 __thread。
那既然 MSVC CRT 支援 multi-thread,因此它應當也有 TLS 囉?我們先看 _beginthread 的程式碼(crt/src/thread.c):
_ptiddata ptd; /* pointer to per-thread data */
一開始便宣告了一個 ptd,註解為指向 per-thread data 的指標。很明顯這就是 TLS 囉!
ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))
指下來分配空間。
_initptd(ptd, _getptd()->ptlocinfo);
初始化 ptd。那這個 struct _tiddata 存了什麼呢?我們看看以下的摘錄(crt/src/mtdll.h):
struct _tiddata {
unsigned long _tid; /* thread ID */
uintptr_t _thandle; /* thread handle */
int _terrno; /* errno value */
unsigned long _tdoserrno; /* _doserrno value */
unsigned int _fpds; /* Floating Point data segment */
unsigned long _holdrand; /* rand() seed value */
char * _token; /* ptr to strtok() token */
wchar_t * _wtoken; /* ptr to wcstok() token */
unsigned char * _mtoken; /* ptr to _mbstok() token */
/* following pointers get malloc'd at runtime */
char * _errmsg; /* ptr to strerror()/_strerror() buff */
wchar_t * _werrmsg; /* ptr to _wcserror()/__wcserror() buff */
char * _namebuf0; /* ptr to tmpnam() buffer */
wchar_t * _wnamebuf0; /* ptr to _wtmpnam() buffer */
char * _namebuf1; /* ptr to tmpfile() buffer */
wchar_t * _wnamebuf1; /* ptr to _wtmpfile() buffer */
char * _asctimebuf; /* ptr to asctime() buffer */
wchar_t * _wasctimebuf; /* ptr to _wasctime() buffer */
void * _gmtimebuf; /* ptr to gmtime() structure */
char * _cvtbuf; /* ptr to ecvt()/fcvt buffer */
unsigned char _con_ch_buf[MB_LEN_MAX];
/* ptr to putch() buffer */
unsigned short _ch_buf_used; /* if the _con_ch_buf is used */
/* following fields are needed by _beginthread code */
void * _initaddr; /* initial user thread address */
void * _initarg; /* initial user thread argument */
}
很明顯啦!這個結構存放了 thread ID、errno 值、strtok 呼叫位置及使用者 thread 的位址及參數…等。注意 _token 指標,依註解說明假使我們在 thread 使用 strtok 時,必需使用到它。
初始化完 ptd 之後,便會呼叫 Windows API CreateThread 來建立一個名稱為 _threadstart 的 thread。這個 thread 待會便會透過 ptd->_initaddr 來呼叫使用者的 thread。
_ptd = (_ptiddata)__fls_getvalue(__get_flsindex())
在 _threadstart 中,首先我們透過 __fls_getvalue 這個 wrapper routine 來取得 ptd。事實上它最後會呼叫到 Windows API TlsGetValue,傳回本身 thread 的 TLS。因此我們可以知道,其實 ptd 就是 TLS 啦!
傳回 _ptd 後,將剛剛我們初始化好的 ptd 值設定給它。這邊大致上來說設定了三個值:使用者 thread 位址、參數以及 thread handler。
_ptd->_initaddr = ((_ptiddata) ptd)->_initaddr;
_ptd->_initarg = ((_ptiddata) ptd)->_initarg;
_ptd->_thandle = ((_ptiddata) ptd)->_thandle;
設定完畢後 ptd 就被釋放了。但注意的是,_ptd 也是要被釋放掉的喔!(因為既然用 TlsGetValue 來取值,代表之前一定有一個對應的 TlsAlloc) 這個待會就會看到了。
_threadstart 最後呼叫 _callthreadstart 這個函式。它裡頭很簡單的就是直接呼叫 ptd->_initaddr。
那我們來看看 _endthread。剛剛說的 _ptd 需要被釋放掉,因此在這個函式可以看見:
_freeptd(ptd)
果然,有做釋放的動作。
說了一堆,我們就可以看到。_beginthread 與 CreateThread 最大的差別就在於 _tiddata 結構的建立與消滅。一般在 Windows 上都會用到 MSVC CRT,如果我們使用 CreateThread 的話,那就是沒有建立 TLS 空間,這樣假設我們使用 strtok 函式的時候,不就會出錯了嗎?但事實上並不會耶!原因就是 strtok 它自己會去檢查這個 _tiddata 存不存在,如果不存在就會自己建立一個。然而,strtok 並不會去釋放掉這個結構(因為連串的呼叫 strtok 時還要用到)。這樣不就會造成 memory leak 嗎?
但實際上假如我們是用 CRT DLL 的方式,在啟動及結束時都會呼叫到 DllMain。DllMain 會自動去檢查 thread 內是否有未釋放的資源,有的話就去釋放。因此使用 DLL 方式時是不會有問題的。問題就出在使用 static 方式時,如果用 CreateThread 並在 thread 中用到需要 _tiddata 結構的函式時,那麼就會出現 memory leak!至於是哪些函式會造成問題,看看在 _tiddata 結構內有保留指標的函式就是囉!
總結,當我們使用 CRT 時(應該都會用吧!),thread 最好使用 _beginthread/_beginthreadex/_endthread/_endthreadex 這組函式來新建和消滅。
留言
張貼留言