爲什麼使用CreateThread時不建議調用RTC函數

原文地址:http://blog.csdn.net/allen_1986/article/details/6530329

改變了棧的大小,但是把CreateThread的第2參數改成0x100000或者更小的時候,程序還是會出現這樣的問題,只有將棧的大小還原爲默認值,且CreateThread的第2參數爲0 才能正確運行

詳細的請查看:http://topic.csdn.net/u/20090905/15/7bf41679-3ed9-40b5-ac71-5f11c088984c.html

 

微軟在Windows API中提供了建立新的線程的函數CreateThread, 
  概述: 
  當使用CreateProcess調用時,系統將創建一個進程和一個主線程。CreateThread將在主線程的基礎上創建一個新線程,大致做如下步驟: 
  1在內核對象中分配一個線程標識/句柄,可供管理,由CreateThread返回 
  2把線程退出碼置爲STILL_ACTIVE,把線程掛起計數置1 
  3分配context結構 
  4分配兩頁的物理存儲以準備棧,保護頁設置爲PAGE_READWRITE,第2頁設爲PAGE_GUARD 
  5lpStartAddr和lpvThread值被放在棧頂,使它們成爲傳送給StartOfThread的參數 
  6把context結構的棧指針指向棧頂(第5步)指令指針指向startOfThread函數 
  MSDN中CreateThread原型: 
  HANDLE CreateThread( 
  LPSECURITY_ATTRIBUTES lpThreadAttributes, 
  DWORD dwStackSize, 
  LPTHREAD_START_ROUTINE lpStartAddress, 
  LPVOID lpParameter, 
  DWORD dwCreationFlags, 
  LPDWORD lpThreadId); 
  參數說明: 
  lpThreadAttributes:指向SECURITY_ATTRIBUTES型態的結構的指針。在Windows 98中忽略該參數。在Windows NT中,它被設爲NULL,表示使用缺省值。 
  dwStackSize,線程堆棧大小,一般=0,在任何情況下,Windows根據需要動態延長堆棧的大小。 
  lpStartAddress,指向線程函數的指針,形式:@函數名,函數名稱沒有限制,但是必須以下列形式聲明: 
  DWORD WINAPI ThreadProc (LPVOID pParam) ,格式不正確將無法調用成功。 
  lpParameter:向線程函數傳遞的參數,是一個指向結構的指針,不需傳遞參數時,爲NULL。 
  dwCreationFlags :線程標誌,可取值如下 
  CREATE_SUSPENDED: 創建一個掛起的線程 
  0 :創建後立即激活。 
  lpThreadId:保存新線程的id。 
  返回值: 
  函數成功,返回線程句柄;函數失敗返回false。 
  函數說明: 
  創建一個線程。 
  語法: 
  hThread = CreateThread (&security_attributes, dwStackSize, ThreadProc,pParam, dwFlags, &idThread) ; 
  一般並不推薦使用 CreateTheard函數,而推薦使用RTL 庫裏的System單元中定義的 BeginTheard函數,因爲這除了能創建一個線程和一個入口函數以外,還增加了幾項保護措施。

程序員對於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): 

#include 
#include 
#include 
#include 

volatile bool threadStarted = false; 

void leaker() 

std::cout << atoi( "0" ) << std::endl; 


DWORD __stdcall CreateThreadFunc( LPVOID ) 

leaker(); 
threadStarted = false; 
return 0; 


DWORD __stdcall CreateThreadFuncWithEndThread( LPVOID ) 

leaker(); 
threadStarted = false; 
_endthread(); 
return 0; 


void __cdecl beginThreadFunc( LPVOID ) 

leaker(); 
threadStarted = false; 


int main() 

for(;;) 

while( threadStarted ) 
Sleep( 5 ); 
threadStarted = true; 
// _beginthread( beginThreadFunc, 0, 0 );//1 
CreateThread( NULL, 0, CreateThreadFunc, 0, 0, 0 );//2 
// CreateThread( NULL, 0, CreateThreadFuncWithEndThread, 0, 0, 0 );//3 

return 0; 


如果你用VC的多線程+靜態鏈接CRT選項去編譯這個程序,並且嘗試打開1、2、3之中的一行,你會發覺只有2打開的情況下,程序纔會發生內存泄漏(可以在Task Manager裏面明顯的觀察到)。3之所以不會出現內存泄漏是因爲主動調用了_endthread。 

VII. 總結 

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

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

CreateThread 是一個Win 32API 函數,_beginthread 是一個CRT(C Run-Time)函數,他們都是實現多線城的創建的函數,而且他們擁有相同的使用方法,相同的參數列表。 

但是他們有什麼區別呢? 

一般來說,從使用角度是沒有多大的區別的,CRT函數中除了signal()函數不能在CreateThread創建的線城中使用外,其他的CRT函數都可一正常使用,但是如果在CreateThread創建的線城中使用CRT函數的話,會產生一些Memory Leak. 

下面是摘自KB的原話: 

SUMMARY 
All C Run-time functions except the signal() function work correctly when used in threads created by the CreateThread() function. However, depending on what CRT functions are called, there may be a small memory leak when threads are terminated. Calling strlen(), for example, does not trigger the allocation of the CRT thread data-block, and calling malloc(), fopen(), _open(), strtok(), ctime(), or localtime() causes allocation of a CRT per-thread data-block, which may cause a memory leak. 
MORE INFORMATION 
The "Programming Techniques" manual supplied with Visual C++ 32-bit Edition states that using CreateThread() in a program that uses Libcmt.lib causes many CRT functions to fail. Actually, the only function that should not be used in a thread created with CreateThread() is the signal() function. 

There are two ways to create threads. One method involves using the CRT _beginthread() or _beginthreadex() (with Visual C++ 2.0 and later); the other method involves using the CreateThread() API. All CRT functions other than the signal() function work correctly in threads created with either _beginthread() or CreateThread(). However, there are some problems involved with using CRT functions in threads created with CreateThread(). 

Threads that are created and terminated with the CreateThread() and ExitThread() Win32 API functions do not have memory that is allocated by the CRT for static data and static buffers cleaned up when the thread terminates. Some examples of this type of memory are static data for errno and _doserrno and the static buffers used by functions such as asctime(), ctime(), localtime(), gmtime(), and mktime(). Using CreateThread() in a program that uses the CRT (for example, links with LIBCMT.LIB) may cause a memory leak of about 70-80 bytes each time a thread is terminated. 

To guarantee that all static data and static buffers allocated by the CRT are cleaned up when the thread terminates, _beginthreadex() and _endthreadex() should be used when creating a thread. The _beginthreadex() function includes the same parameters and functionality as CreateThread(). 

另外有個小小的測驗: 
用CreateThread 創建的線城能否被CRT函數 _endthreadex() 關閉? 
CreateThread、_beginthread和_beginthreadex都是用來啓動線程的,但大家看到oldworm沒有提供_beginthread的方式,原因簡單,_beginthread是_beginthreadex的功能子集,雖然_beginthread內部是調用_beginthreadex但他屏蔽了象安全特性這樣的功能,所以_beginthread與CreateThread不是同等級別,_beginthreadex和CreateThread在功能上完全可替代,我們就來比較一下_beginthreadex與CreateThread! 

CRT的函數庫在線程出現之前就已經存在,所以原有的CRT不能真正支持線程,這導致我們在編程的時候有了CRT庫的選擇,在MSDN中查閱CRT的函數時都有: 
Libraries 
LIBC.LIB Single thread static library, retail version 
LIBCMT.LIB Multithread static library, retail version 
MSVCRT.LIB Import library for MSVCRT.DLL, retail version 
這樣的提示! 
對於線程的支持是後來的事! 
這也導致了許多CRT的函數在多線程的情況下必須有特殊的支持,不能簡單的使用CreateThread就OK。 
大多的CRT函數都可以在CreateThread線程中使用,看資料說只有signal()函數不可以,會導致進程終止!但可以用並不是說沒有問題! 

有些CRT的函數象malloc(), fopen(), _open(), strtok(), ctime(), 或localtime()等函數需要專門的線程局部存儲的數據塊,這個數據塊通常需要在創建線程的時候就建立,如果使用CreateThread,這個數據塊就沒有建立,然後會怎樣呢?在這樣的線程中還是可以使用這些函數而且沒有出錯,實際上函數發現這個數據塊的指針爲空時,會自己建立一個,然後將其與線程聯繫在一起,這意味着如果你用CreateThread來創建線程,然後使用這樣的函數,會有一塊內存在不知不覺中創建,遺憾的是,這些函數並不將其刪除,而CreateThread和ExitThread也無法知道這件事,於是就會有Memory Leak,在線程頻繁啓動的軟件中(比如某些服務器軟件),遲早會讓系統的內存資源耗盡! 

_beginthreadex(內部也調用CreateThread)和_endthreadex就對這個內存塊做了處理,所以沒有問題!(不會有人故意用CreateThread創建然後用_endthreadex終止吧,而且線程的終止最好不要顯式的調用終止函數,自然退出最好!) 

談到Handle的問題,_beginthread的對應函數_endthread自動的調用了CloseHandle,而_beginthreadex的對應函數_endthreadex則沒有,所以CloseHandle無論如何都是要調用的不過_endthread可以幫你執行自己不必寫,其他兩種就需要自己寫!(Jeffrey Richter強烈推薦儘量不用顯式的終止函數,用自然退出的方式,自然退出當然就一定要自己寫CloseHandle)

發佈了60 篇原創文章 · 獲贊 19 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章