該用 _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 這組函式來新建和消滅。


留言

熱門文章