標籤:
在寫c++代碼時,一直牢記著一句話:決不應該調用CreateThread。 應該使用Visual C++運行期庫函數_beginthreadex。好像CreateThread函數就是老虎,既然這樣為什麼微軟要開發這個函數呢?
不要用 CreateThread 建立線程、並用 CloseHandle 來關閉這個線程,因為這樣會導致記憶體泄露,而應該用 _beginthread 來建立線程,_endthread 來銷毀線程。其實,真正的原因並非如此。
因為CreateThread 後,線程終止運行後,線程對象仍然在系統中,必須通過 CloseHandle 函數來關閉該線程對象。為什麼會引起記憶體泄露呢?因為當線程的函數用到了C的標準庫的時候,很容易導致衝突,所以在建立VC的工程時,系統提示是用單線程還是用多線程的庫,因為在C的內部有很多的全域變數。例如 errno。
因為在C的庫中有全域變數,如果程式中使用了標準的C的庫時,很容易導致運行不正常。所以,微軟和Borland都對C的庫進行了一些改進。但是這個改進的一個條件就是,如果一個線程已經開始建立,就應該建立一個結構來包含這些全域變數,接著把這些全域變數放入線程的上下文中。從而全域變數就會依賴於這個線程,不會引起衝突。
這樣做就會有一個問題,什麼時候這個線程開始建立呢?標準的C庫是不知道的,而 CreateThread 是作業系統的API。所以需要使用 _beginthread/_endthread 來建立/結束線程,讓標準C庫為線程化做些準備工作。
當用 _beginthread 來建立,而用 CloseHandle 來關閉線程時,標準C庫複製的全域變數就不會被釋放,這才是前面說的記憶體泄露的原因。
另一方面,如果用 CreateThread/CloseHandle 來建立/結束線程,則不要使用標準C庫的任何函數。還有一個需要注意的,就是線上程執行完之前,不要使用 CloseHandle 來結束線程,否則也會有異常。我們一般通過 WaitForSingleObject/WaitForMultipleObjects 來等待線程結束。
CreateThread函數是用來建立線程的Windows函數。不過,如果你正在編寫C/C++代碼,決不應該調用CreateThread。相反,應該使用Visual C++運行期庫函數_beginthreadex。如果不使用Microsoft的Visual C++編譯器,你的編譯器供應商有它自己的CreateThred替代函數。
若要使多線程C和C++程式能夠正確地運行,必須建立一個資料結構,並將它與使用C/C++運行期庫函數的每個線程關聯起來。當你調用C/C++運行期庫時,這些函數必須知道查看調用線程的資料區塊,這樣就不會對別的線程產生不良影響。
1.每個線程均獲得由C/C++運行期庫的堆棧分配的自己的tiddata記憶體結構。
2.傳遞給_beginthreadex的線程函數的地址儲存在tiddata記憶體塊中。傳遞給該函數的參數也儲存在該資料區塊中。
3._beginthreadex確實從內部調用CreateThread,因為這是作業系統瞭解如何建立新線程的唯一方法。
4.當調用CreatetThread時,它被告知通過調用_threadstartex而不是pfnStartAddr來啟動執行新線程。 還有,傳遞給線程函數的參數是tiddata結構而不是pvParam的地址。 5.如果一切順利,就會像CreateThread那樣返回線程控制代碼。如果任何操作失敗了,便返回 NULL。
_beginthreadex和_beginthread函數的區別。_beginthread函數的參數比較少,因此位元性全面的_beginthreadex函數受到更大的限制。
例如,如果使用_beginthread,就無法建立帶有安全屬性的新線程,無法建立暫停線程,也 無法獲得線程的ID值。 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)線程的記憶體流失的主要原因
在很多參考書上,都說不要用CreateThread 建立線程、並用CloseHandle來關閉這個線程,因為這樣做會導致記憶體流失,而應該用_beginthread來建立線程,_endthread來銷毀線程。其實,真正的原因並非如此。
看如下一段代碼:
HANDLE CreateThread
(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全執行緒屬性
DWORD dwStackSize, // 堆棧大小
LPTHREAD_START_ROUTINE lpStartAddress, // 線程函數
LPVOID lpParameter, //線程參數
DWORD dwCreationFlags, // 線程建立屬性
LPDWORD lpThreadId // 線程ID
);
線程中止運行後,線程對象仍然在系統中,必須通過CloseHandle函數來關閉該線程對象。
CloseHandle函數的原型是:
BOOL CloseHandle( HANDLE hObject );//HANDLE hObject 物件控點
CloseHandle可以關閉多種類型的對象,比如檔案對象等,這裡使用這個函數來關閉線程對象。調用時,hObject為待關閉的線程對象的控制代碼。
說用這種方法時記憶體在泄漏,其實不完全正確。那為什麼會引起記憶體的泄漏呢?因為當線程的函數用到了C的標準庫的時候,很容易導致衝突,所以在建立VC的工程時,系統提示是用單線程還是用多線程的庫,因為在C的內部有很多的全域變數。例如,出錯號、檔案控制代碼等全域變數。
因為在C的庫中有全域變數,這樣用C的庫時,如果程式中使用了標準的C的庫時,就很容易導致運行不正常,會引起很多的衝突。所以,微軟和Borland都對C的庫進行了一些改進。但是這個改進的一個條件就是,如果一個線程已經開始建立了,就應該建立一個結構來包含這些全域變數,接著把這些全域變數放入線程的上下文中和這個線程相關起來。這樣,全域變數就會依賴於這個線程,不會引起衝突。 這樣做就會有一個問題,什麼時候這個線程開始建立呢?標準的Windows的API是不知道的,因為它是靜態庫。這些庫都是放在VC的LIB的目錄內的,而線程函數是作業系統的函數。所以,VC和BC在建立線程時,都會用_beginThread來建立線程,再用_endThread來結束線程。
這樣,它們在建立線程的時候,就會知道什麼時候建立了線程,並把全域變數放入某一結構中,讓它和線程能關聯起來。這樣就不會發生衝突了。
很顯然,要完成這個功能,首先需要分配結構表把全域變數包含起來。這個過程是在_beginThread時做的,而釋放在_endTread內完成。
所以,當用_beginThread來建立,而用CloseHandle來關閉線程時,這時複製的全域結構就不會被釋放了,這就有了記憶體的泄漏。這就是很多資料所說的記憶體流失問題的真正的原因。
其實,可以不用_beginThread和_endThread這一對函數。
如果用CreateThread函數建立,用CloseHandle關閉,那麼,與C有關的庫就會用全域的,它們會引起衝突。所以,比較好的方法就是線上程內不用標準的C的庫(可以使用Windows API的庫函數)。這樣就不會有什麼問題,也就不會引起衝突。例如,字串的操作函數、檔案操作等。
當某個程式建立一個線程後,會產生一個線程的控制代碼,線程的控制代碼主要用來控制整個線程的運行,例如停止、掛起或設定線程的優先順序等操作。一般來說,當線程啟用後,就會用線程的CloseHandle來關閉線程。
但在微軟的樣本程式中,有一個例子建立以後,就馬上調用CloseHandle關閉線程的運行。這樣做在Windows 98下沒什麼問題,但在Windows NT下,核心就會出現錯誤。這是為什麼呢?
這是因為雖然線程有關的結構已經釋放了,但線程還在運行中,所以程式就會出現錯誤。那怎麼做才能確保正常運行呢? 其實,要正常運行,可以讓線程完全結束以後,再調用CloseHandle來釋放資源。 怎樣知道線程完全結束呢?在Windows 的API中有一類等待線程的命令:
DWORD WaitForSingleObject
(
HANDLE hHandle, // handle to object to wait for
DWORD dwMilliseconds , // time-out interval in milliseconds
);
DWORD WaitForMultipleObjects
(
DWORD nCount, // number of handles in the handle array
CONST HANDLE *lpHandles, // pointer to the object-handle array
BOOL fWaitAll, // wait flag
DWORD dwMilliseconds // time-out interval in milliseconds
);
可以用以上兩函數,等待線程的結束。如果線程結束,函數就會返回。否則就一直等待,直到指定的時間結束。 還有一種線程根本不會退出,它一直運行著迴圈的線程。我們就要用中止線程的方法來結束線程的運行,強制把它關閉。強制關閉後,再用CloseHandle來釋放結構。
觀眾看點:為何要用_beginthreadex()而非CreateThread?
當你打算實現一個多線程(非MFC)程式,你會選擇一個單線程的CRT(C執行階段程式庫)嗎?如果你的回答是NO, 那麼會帶來另外一個問題,你選擇了CreateThread來建立一個線程嗎? 大多數人也許會立刻回答YES。可是很不幸,這是錯誤的選擇。
我先來說一下我的結論,待會告訴你為什麼。
如果要作多線程(非MFC)程式,在主線程以外的任何線程內
- 使用malloc(),free(),new
- 調用stdio.h或io.h,包括fopen(),open(),getchar(),write(),printf(),errno
- 使用浮點變數和浮點運算函數
- 調用那些使用靜態緩衝區的函數如: asctime(),strtok(),rand()等。
你就應該使用多線程的CRT並配合_beginthreadex(該函數只存在於多線程CRT), 其他情況下你可以使用單線程的CRT並配合CreateThread。
因為對產生的線程而言,_beginthreadex比之CreateThread會為上述操作多做額外的簿記工作,比如協助strtok()為每個線程準備一份緩衝區。
然而多線程程式極少情況不使用上述那些函數(比如記憶體配置或者io),所以與其每次都要思考是要使用_beginthreadex還是CreateThread,不如就一棍子敲定_beginthreadex。
當然你也許會藉助win32來處理記憶體配置和Io,這時候你確實可以以單線程crt配合CreateThread,因為io的重任已經從crt轉交給了win32。這時通常你應該使用HeapAlloc,HeapFree來處理記憶體配置,用CreateFile或者GetStdHandle來處理Io。
還有一點比較重要的是_beginthreadex傳回的雖然是個unsigned long,其實是個線程Handle(事實上_beginthreadex在內部就是調用了CreateThread),所以你應該用CloseHandle來結束他。千萬不要使用ExitThread()來退出_beginthreadex建立的線程,那樣會喪失釋放簿記資料的機會,應該使用_endthreadex.
××××××××××××××××××××××××××××××××××××××
說簡單點,當線程異常結束的時候,_beginthread建立的線程會調用棧上變數的解構函式,釋放資源,而CreateThread建立的線程不會。因為CreateThread是作業系統提供的介面,要效率,要速度,他不知道堆棧這玩意,無心處理這些,也許某種語言的記憶體模式跟C/C++完全不一樣,所以CreateThread不會去管C/C++的東西;但CRT為了保證一致性,C/C++變數的析構是他的責任,所以CRT得管,封裝了強化了CreateThread,保證一致性。
××××××××××××××××××××××××××××××××××××××××××××××××××
具體說來,CreateThread這個 函數是windows提供給使用者的 API函數,是SDK的標準形式,在使用的過程中要考慮到進程的同步與互斥的關係,進程間的同步互斥等一系列會導致作業系統死結的因素,用起來比較繁瑣一些,初學的人在用到的時候可能會產生不可預料的錯誤,建議多使用AfxBeginThread,是編譯器對原來的CreateThread函數的封裝,用與MFC編程(當然,只要修改了項目屬性,console和win32項目都能調用)而_beginthread是C的運行庫函數。
在使用AfxBeginThread時,線程函數的定義為:UINT _yourThreadFun(LPVOID pParam)參數必須如此
在使用CreateThread時,線程的函數定義為: DWORD WINAPI _yourThreadFun(LPVOID pParameter)。
兩個的實質都是一樣的,不過AfxBeginThread返回一個CWinThread的指標,就是說他會new一個CWinThread對象,而且這個對象是自動刪除的(線上程運行結束時),給我們帶來的不便就是無法獲得它的狀態,因為隨時都有可能這個指標指向的是一個已經無效的記憶體地區,所以使用時(如果需要瞭解它的健全狀態的話)首先CREATE_SUSPENDED讓他掛起,然後m_bAutoDelete=FALSE,接著才ResumeThread,最後不要了delete那個指標。
CreatThread就方便多了,它返回的是一個控制代碼,如果你不使用CloseHandle的話就可以通過他安全的瞭解線程狀態,最後不要的時候CloseHandle,Windows才會釋放資源,所以我一般使用CreatThread,方便。
如果用MFC編程,不要用CreateThread,如果只是使用Runtime Library,用_BegingThread,總之,不要輕易使用CreateThread。這是因為在MFC和RTL中的函數有可能會用到些它們所封裝的公用變數,也就是說AfxBeginThread和_BeginThread都有自己的啟動代碼是CreateThread所沒有的。
在用CreateThread所建立的線程中使用MFC的類和RTL函數就有可能出現問題。
如果你是用彙編編寫win32程式並且線上程函數中也不調用MFC和RTL的函數,那用CreateThread就沒問題,或者你雖然是用C寫線程函數,但你很小心沒調用RTL函數也不會有問題。
CreateThread是由作業系統提供的介面,而AfxBeginThread和_BeginThread則是編譯器對它的封裝。
在可能的情況下,不要調用_beginthread,而應該調用_beginthreadex,以及對應的_endthreadex。這都是C++運行期函數。
但是使用_beginthread,無法建立帶有安全屬性的新線程,無法建立暫停線程,也無法獲得線程ID,_endthread的情況類似,它不帶參數,
這意味這線程的結束代碼必須寫入程式碼為0。這兩個函數在_beginthreadex和_endthreadex中進行調用。CreateThread不要進行直接調用。
(轉)CreateThread與_beginthread,記憶體流失為何因(原帖排版有些不好 ,所以我稍微整理下)