_beginthread 和 CreateThread 區別【轉】


/*-------------------------------------------------------------------------------------------------

摘錄時間    2017-03-03;
_beginthread 和 CreateThread 區別;
程序員對於Windows程序中應該用_beginthread還是CreateThread來創建線程,一直有所爭論;
本文將從對CRT源代碼出發探討這個問題;

I. 起因;
今天一個朋友問我程序中究竟應該使用_beginthread還是CreateThread,並且告訴我如果使用不當可能會有內存泄漏;
其實我過去對這個問題也是一知半解,為了對朋友負責,專門翻閱了一下VC的運行庫(CRT)源代碼,終於找到了答案;

II. CRT;
CRT(C/C++ Runtime Library)是支持C/C++運行的一系列函數和代碼的總稱;
雖然沒有一個很精確的定義,但是可以知道,你的main就是它負責調用的,你平時調用的諸如strlen、strtok、time、atoi之類的函數也是它提供的;
我們以Microsoft Visual.NET 2003中所附帶的CRT為例;
假設你的.NET 2003安裝在C:Program FilesMicrosoft Visual Studio.NET 2003中,那么CRT的源代碼就在C : Program FilesMicrosoft Visual Studio.NET 2003Vc7crtsrc中;
既然有了這些實現的源代碼,我們就可以找到一切解釋了;

III. _beginthread / _endthread;
這個函數究竟做了什么呢?它的代碼在thread.c中;
閱讀代碼,可以看到它最終也是通過CreateThread來創建線程的,主要區別在於,它先分配了一個_tiddata,並且調用了_initptd來初始化這個分配了的指針;
而這個指針最后會被傳遞到CRT的線程包裝函數_threadstart中,在那里會把這個指針作為一個TLS(Thread Local Storage)保存起來,然后_threadstart會調用我們傳入的線程函數,並且在那個函數退出后調用_endthread;
這里也可以看到,_threadstart用一個__try / __except塊把我們的函數包了起來,並且在發生異常的時候,調用exit退出。(_threadstart和endthread的代碼都在thread.c中)
這個_tiddata是一個什么樣的結構呢?
它在mtdll.h中定義,它的成員被很多CRT函數所用到,譬如int _terrno,這是這個線程中的錯誤標志;char* _token,strtok以來這個變量記錄跨函數調用的信息...
那么_endthread又做了些什么呢?
除了調用浮點的清除代碼以外,它還調用了_freeptd來釋放和這個線程相關的tiddata。也就是說,在 _beginthread里面分配的這塊內存,以及在線程運行過程中其它CRT函數中分配並且記錄在這個內存結構中的內存,在這里被釋放了。

通過上面的代碼,我們可以看到,如果我使用_beginthread函數創建了線程,它會為我創建好CRT函數需要的一切,並且最后無需我操心,就可以把清除工作做得很好;
可能唯一需要注意的就是,如果需要提前終止線程,最好是調用_endthread或者是返回,而不要調用ExitThread,因為這可能造成內存釋放不完全;
同時我們也可以看出,如果我們用CreateThread函數創建了線程,並且不對C運行庫進行調用(包括任何間接調用),就不必擔心什么問題了;

IV. CreateThread和CRT
或許有人會說,我用CreateThread創建線程以后,我也調用了C運行庫函數,並且也使用ExitThread退出了,可是我的程序運行得好好的,既沒有因為CRT沒有初始化而崩潰,也沒有因為忘記調用 _endthread而發生內存泄漏.
這是為什么呢,讓我們繼續我們的CRT之旅。
假設我用CreateThread創建了一個線程,我調用 strtok函數來進行字符串處理,這個函數肯定是需要某些額外的運行時支持的。strtok的源代碼在strtok.c中。
從代碼可見,在多線程情況下,strtok的第一句有效代碼就是_ptiddata ptd = _getptd(),它通過這個來獲得當前的ptd.
可是我們並沒有通過_beginthread來創建ptd,那么一定是_getptd搗鬼了。打開 tidtable.c,可以看到_getptd的實現,果然,它先嘗試獲得當前的ptd,如果不能,就重新創建一個。因此,后續的CRT調用就安全了。
可是這塊ptd最終又是誰釋放的呢?打開dllcrt0.c,可以看到一個DllMain函數。在VC中,CRT既可以作為一個動態鏈接庫和主程序鏈接,也可以作為一個靜態庫和主程序鏈接,這個在Project Setting->Code Generations里面可以選。
當CRT作為DLL鏈接到主程序時,DllMain就是CRT DLL的入口。Windows的DllMain可以由四種原因調用:Process Attach / Process Detach / Thread Attach / Thread Detach;
最后一個,也就是當線程函數退出后但是線程還沒有銷毀前,會在這個線程的上下文中用Thread Detach調用DllMain,這里,CRT做了一個_freeptd(NULL),也就是說,如果有ptd,就free掉;
所以說,恰巧沒有發生內存泄漏是因為你用的是動態鏈接的CRT。
於是我們得出了一個更精確的結論,如果我沒有使用那些會使用_getptd的CRT函數,使用CreateThread就是安全的。

V. 使用ptd的函數;
那么,究竟那些函數使用了_getptd呢?很多!在CRT目錄下搜索_getptd,你會發覺很多意想不到的函數都用到了它,除了strtok、rand這類需要保持狀態的,還有所有的字符串相關函數,因為它們要用到ptd中的locale信息;
所有的mbcs函數,因為它們要用到ptd中的mbcs信息...;

VI. 測試代碼;
最下面是一段測試代碼(leaker中用到了atoi,它需要ptd);
如果你用VC的多線程+靜態鏈接CRT選項去編譯這個程序,並且嘗試打開1、2、3之中的一行,你會發覺只有2打開的情況下,程序才會發生內存泄漏(可以在Task Manager里面明顯的觀察到);
3之所以不會出現內存泄漏是因為主動調用了_endthread;

VII. 總結;
如果你使用了DLL方式鏈接的CRT庫,或者你只是一次性創建少量的線程,那么你或許可以采取鴕鳥策略,忽視這個問題;
上面一節代碼中第3種方法基於對CRT庫的了解,但是並不保證這是一個好的方法,因為每一個版本的VC的CRT可能都會有些改變;
看來,除非你的頭腦清晰到可以記住這一切,或者你可以不厭其煩的每調用一個C函數都查一下CRT代碼,否則總是使用 _beginthread(或者它的兄弟_beginthreadex)是一個不錯的選擇;

VIII. 后記;
網友condor指出本文的一個錯誤:在dllcrt0.c 中,DllMain的Thread Detach所釋放的ptd,其實是dllcrt0.c的DllMain中的Thread Attach所創建的;
也就是說,當你用CRT DLL的時候,DllMain對線程做了一切初始化 / 清除工作。我查看源代碼,thread.c中的_threadstart函數,在設置TLS之前做了檢查,這其實就是為了避免重復設置導致的內存泄漏;

-------------------------------------------------------------------------------------------------*/


// MyThread: my_thread.h
/*-------------------------------------------------------------------------------------------------

#include <process.h>
#include <iostream>
#include <CRTDBG.H>
#include <wtypes.h>

#ifdef MYTHREAD_EXPORTS
#define MYTHREAD_API  _declspec(dllexport)
#else
#define MYTHREAD_API  _declspec(dllimport)
#endif

extern volatile bool threadStarted = false;

MYTHREAD_API void my_printf();

MYTHREAD_API DWORD __stdcall CreateThreadFunc(LPVOID);

MYTHREAD_API DWORD __stdcall CreateThreadFuncWithEndThread(LPVOID);

MYTHREAD_API void __cdecl beginThreadFunc(LPVOID);

-------------------------------------------------------------------------------------------------*/


// MyThread: my_thread.cpp
/*-------------------------------------------------------------------------------------------------

#include "my_thread.h"

void my_printf()
{
    std::cout << atoi("0") << std::endl;
}

DWORD __stdcall CreateThreadFunc(LPVOID)
{
    my_printf();
    threadStarted = false;
    return 0;
}

DWORD __stdcall CreateThreadFuncWithEndThread(LPVOID)
{
    my_printf();
    threadStarted = false;
    _endthread();
    return 0;
}

void __cdecl beginThreadFunc(LPVOID)
{
    my_printf();
    threadStarted = false;
}

-------------------------------------------------------------------------------------------------*/


// Thread_Test: thread_test.cpp
/*-------------------------------------------------------------------------------------------------

#include "my_thread.h"

#define BEGINTHREAD_TEST
#define CREATETHREAD_TEST
#define CREATETHREAD_WITHENDTHREAD_TEST

int main()
{
    while (1)
    {
        while (threadStarted)
        {
            Sleep(5);
        }
        threadStarted = true;

#ifdef BEGINTHREAD_TEST
        _beginthread( beginThreadFunc, 0, 0 );//1
#endif

#ifdef CREATETHREAD_TEST
        CreateThread(NULL, 0, CreateThreadFunc, 0, 0, 0);//2
#endif

#ifdef CREATETHREAD_WITHENDTHREAD_TEST
        CreateThread( NULL, 0, CreateThreadFuncWithEndThread, 0, 0, 0 );//3
#endif

        break;
    }
    getchar();
    return 0;
}

-------------------------------------------------------------------------------------------------*/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM