標籤:繼承 str 圖文 ada port writable ons 函數 tween
一、線程建立
Windows線程在建立時會首先建立一個線程核心對象,它是一個較小的資料結構,作業系統通過它來管理線程。新線程可以訪問進程核心對象的所有控制代碼、進程中的所有記憶體及同一進程中其它線程的棧。
建立有以下幾種方式,分別說明
- CreateThread(...) (作業系統提供的API,盡量不要使用)
- _beginthread(...)
- _beginthreadex(...)
- AfxBeginThread(...) (MFC提供的介面)
首先聲明一個線程函數,原型為:
DWORD FunThread(LPVOID pParam);
1. CreateThread()
該函數為作業系統提供,原型如下:
HANDLE WINAPI CreateThread( _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes , _In_ SIZE_T dwStackSize , _In_ LPTHREAD_START_ROUTINE lpStartAddress, _In_opt_ LPVOID lpParameter, _In_ DWORD dwCreationFlags, _Out_opt_ LPDWORD lpThreadId );
說明:
Header |
Library |
Dll |
WinBase.h |
Kernel32.lib |
Kernel32.dll |
參數:
lpThreadAttributes:指向SECURITY_ATTRIBUTES結構體的指標,記錄線程的安全描述。決定子進程能否繼承到返回的控制代碼,如果為NULL,則採用預設安全層級(THREAD_PRIORITY_NORMAL),同時返回控制代碼不能繼承
dwStackSize:指定線程棧大小,當為0時,表示棧使用預設大小
lpStartAddress:線程函數指標
lpParameter:線程函數參數
dwCreationFlags:為0:表示線程建立後立即運行;為CREATE_SUSPEND:建立後掛起,此時可修改線程屬性,通過ResumeThread喚醒;
lpThreadId:一個指向threadID的指標,若對線程ID關注,則傳值,否則置NULL
傳回值:
建立線程的控制代碼;
若建立失敗,則返回NULL,可用GetLastError()捕獲錯誤;
MFC中也提供了CreateThread函數,它是CWinThread類的一個方法,如下
BOOL CreateThread( DWORD dwCreateFlags = 0, UINT nStackSize = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
參數含義與傳回值含義一致,它的調用方式是:
CWinThread thread1;thread1.CreateThread();
需要說明的是dwCreateFlags傳值為CREATE_SUSPEND時, 要通過CWinThread::ResumeThread來喚醒
2. _beginthread(), _beginthreadex()
原型:
unsigned long _beginthread( void( __cdecl *start_address )( void * ), unsigned stack_size, void *arglist );unsigned long _beginthreadex( void *security, unsigned stack_size, unsigned ( __stdcall *start_address )( void * ), void *arglist, unsigned initflag, unsigned *thrdaddr );
說明:
Header |
Library |
process.h |
LIBCMT.lib MSVCRT.lib |
參數與上面CreateThread含義相同,不在贅述;
二者比較:
1. _beginthread中線程函數調用為_cdecl,且無傳回值; _beginthreadex為_stdcall,有傳回值;
2. _beginthreadex中initflag相當於CreateThread中的dwCreationFlags,thrdaddr相當於lpThreadId
3.在實現上_beginthreadex控制了一個_tiddata的線程資料區塊,裡面存放了線程函數地址、參數的很多屬性,之後再間接調用CreateThread(...);
4._beginthread則參數較少;
3. AfxBeginThread()
MFC提供的介面提供了二種不同類型線程的產生,即工作者線程和使用介面執行緒;可以簡單理解使用介面執行緒包含使用者介面,它有自己的訊息佇列,工作者線程用於計算等;
CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc, //線程函數指標,函數原型為UINT _cdecl fnThread(LPVOID pParam); LPVOID pParam, //線程函數參數 int nPriority = THREAD_PRIORITY_NORMAL, //優先順序,SetThreadPriority UINT nStackSize = 0, //棧大小,單位是bytes,為0時表示按預設大小 DWORD dwCreateFlags = 0, //CREATE_SUSPENDED:建立後掛起; 0:建立後立即運行 LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL //指向SECURITY_ATTRIBUTES結構的指標,為Null時表示預設安全屬性); //建立一個工作者線程CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, //指向介面類指標,繼承自CWinThread int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ); //建立一個使用介面執行緒
說明:
4. 比較1. _beginthread與_beginthreadex
- 在實現上_beginthreadex控制了一個_tiddata的線程資料區塊,裡面存放了線程函數地址、參數的很多屬性,之後再間接調用CreateThread(...);
- _beginthread則參數較少,有局限性;
2. AfxBeginThread與mfc的CreateThread
- AfxBeginThread一步建立,之後線程立即運行
- CWinThread::CreateThread二步建立,但它儲存了線程對象,可以在連續的線程建立與運行完成結束之間再使用(Use CreateThread if you want to reuse the thread object between successive creation and termination of thread executions)
二、線程終結
終結線程的幾個方法:
- 等待線程函數運行完成自動結束
- ExitThread(),用於結束線程自身
- TerminateThread(),所有線程都可以用該方法結束
- 父進程關閉,子線程隨之關閉
只建議使用第一種方法結束線程,其它的方式都對應有缺陷
下面給出幾個結束過程中發生事情:
1.資源有序釋放(如作業系統分配資源,用到的C++類析構),返回線程結束代碼,線程核心對象使用計數-1
2.作業系統相關資源釋放;但象C++類並未析構,造成記憶體泄露;這裡如果用_beginthreadex建立線程,而用ExitThread或者_endthread來釋放線程,則線程放在堆上的線程資料區塊_tiddata也未釋放,記憶體泄露;
3.該函數為非同步函數,即通知作業系統終結線程後立即返回,而不管系統是否已經真的結束了線程。同時線程棧也不會釋放
4.用ExitProgerss, TerminateProcess函數關閉進程後,進程會調用TerminateThread來關閉線程,效果如3,線程的棧沒有釋放,申請的對象資源也沒釋放。
1. ExitThread()
VOID ExitThread( DWORD dwExitCode);
說明:
Header |
Library |
winbase.h |
coredll.lib |
參數:
dwExitCode: 指定線程的結束代碼。可以通過GetExitCodeThread來查看一個線程的結束代碼
傳回值:無
說明:線上程結束後,會將線程核心對象中的ExitCode由STILL_ACTIVE轉變為傳入結束代碼;與CreateThread對應
2. TerminateThread()
BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode);
說明:
Header |
Library |
winbase.h |
coredll.lib |
參數:
hThread: 要結束的線程控制代碼
dwExitCode: 指定線程的結束代碼。可以通過GetExitCodeThread來查看一個線程的結束代碼
傳回值:0表示失敗,非0表示成功;
3. 判斷線程是否結束
BOOL GetExitCodeThread( HANDLE hThread, LPDWORD lpExitCode);
//判斷bool IsThreadExit(HANDLE hThread){ bool bRet = false; DWORD dwExitCode; if(GetExitCodeThread(hThread, &dwExitCode)) { if(dwExitCode != STILL_ACTIVE) bRet = true; } else { //error err = GetLastError(); throw err; } return bRet; }
三、注意事項
1.在C++多線程編程中,盡量使用_beginthreadex及_endthreadex,而不是其它介面。
不使用_beginthread原因:
(1)_beginthread函數參數不夠多,某些要求達不到,
不使用_endthread原因:
(1)_endthread函數也是無參的,即線程的結束代碼會被寫入程式碼為0;
(2)該函數在調用ExitThread前,會調用CloseHandle,並傳入新線程的控制代碼。類似下面代碼會有錯誤
DWORD dwExitCode;HANDLE hThread = _beginthreadex(...);GetExitCodeThread(hThread, &dwExitCode);CloseHandle(hThread);
不使用CreateThread函數原因:
(1)標準C/C++運行庫最初並不是為多線程程式而設計的(標準的C執行階段程式庫出現在作業系統對線程支援之前),而CreateThread是作業系統介面,調用它時系統不知道是C/C++來調用的,因此為了保證C/C++程式正常運行,要建立一個資料結構與運行庫的每個線程關聯,_beginthreadex就實現了這樣的功能。換言之,在C/C++中用CreateThread建立線程是極度不安全的。
不使用ExitThread函數原因:
(1)作業系統相關資源釋放;但象C++類並未析構,造成記憶體泄露;這裡如果用_beginthreadex建立線程,而用ExitThread或者_endthread來釋放線程,則線程放在堆上的線程資料區塊_tiddata也未釋放,記憶體泄露;
2.C/C++編程中使用CreateThread會發生什麼
當線程調用一個需要線程資料區塊_tiddata的運行庫函數時,系統會首先通過線程局部儲存(TLS,見下節)來找到線程資料區塊,若為NULL,C/C++運行庫會主調線程分配並初始化一個_tiddata塊並與線程關聯。但若使用C/C++運行庫的signal函數,則整個進程都會終止(因結構化異常處理幀SEH未就緒,RtlUserThreadStart會直接調用ExitProcess來結束進程);此外,若不通過_endthreadex來結束線程,線程資料區塊_tiddata不會釋放,造成記憶體泄露。
參考:
1. <<Windows核心編程(第五版)>>
2. 關於_BEGINTHREADEX、_BEGINTHREAD和CREATETHREAD
3. MFC 多線程及線程同步
4. MSDN
上節中介紹了幾種Windows平台建立及刪除線程的api及它們的差別,這節具體介紹以下資訊:
1.線程核心對象(作業系統介面CreateThread內部實現)
2.線程資料區塊_tiddata(C/C++執行階段程式庫的實現 _beginthreadex與_beginthread)
3.線程結束_endthreadex
下面分別介紹
一、線程核心對象
線程建立時,會先建立一個線程核心對象(分配在進程的地址空間上),如,儲存上下文context(一個資料結構)及一些統計資訊,具體包括:
1.寄存器SP:指向棧中線程函數指標的地址
2.寄存器IP:指向裝載的NTDLL.dll裡RtlUserThreadStart函數地址
3.Usage Count:引用計數,初始化為2
4.Suspend Count:掛起數,初始化為1。
5.ExitCode:結束代碼,線程在運行時為STILL_ACTIVE(且初始化為該值)
6.Signaled:初始化為未觸發狀態
RtlUserThreadStart(...)
函數原型如下:
RtlUserThreadStart函數是線程真正起始執行的地方,因為新線程的指令指標是指向這個函數 。RtlUserThreadStart函數原型使你認為它收到二個參數,但那不是真的,這隻是意味著它被調用自另一個函數。新的線程只不過在這裡生效和開始執行。
RtlUserThreadStart函數之所以認為被調用自另一個函數,是因為它獲得了二個參數。但是獲得這些參數的途徑是由於作業系統把這些值直接寫入了線程的堆棧(通常參數被傳遞到函數的方法)。
注意,一些CPU架構用CPU寄存器來傳遞參數,而不是堆棧。對於這些架構,系統在同意線程執行RtlUserThreadStart函數之前會對相應的寄存器進行正確的初始化。
(在32位Windows中用的是BaseThreadStart函數而不是64位Windows中的RtlUserThreadStart函數,BaseThreadStart函數來出自Kernel32.dll組件,而RtlUserThreadStart函數函數來出自於NTDLL.dll組件)
RtlUserThreadStart函數是線程真正開始執行的地方,在函數中
(1)設定了一個圍繞線程函數的結構化異常處理SEH幀
(2)線程函數返回時,調用ExitThread,並將線程函數傳回值作為參數傳遞進去。線程核心對象的使用計數遞減,而後線程停止執行。
(3)執行期間若發生未被處理的異常,則調用異常處理塊中的ExitProgress()關閉進程
當一個程式運行時,會產生一個主線程,之後RtlUserThreadStart開始執行,調用C/C++運行庫的代碼,後者初始化繼而訪問你的程式入口函數(_tmain,_tWinMain等);入口函數返回時,C/C++運行時啟動代碼會調用ExitProcess來結束進程。
因此使用CreateThread產生線程應有二步
(1)產生線程核心對象並初始化
(2)由核心對象指向的RtlUserThreadStart運行線程函數
二、線程資料區塊_tiddata
線程資料區塊是_beginthreadex函數維護的一個資料結構,儲存了線程相關的一些資訊。我們先來看_beginthreadex的源碼(VS2008的儲存在C:\Program Files\Microsoft Visual Studio 9.0\VC\crt\src\threadex.c中):
_MCRTIMP uintptr_t __cdecl _beginthreadex ( void *security, unsigned stacksize, unsigned (__CLR_OR_STD_CALL * initialcode) (void *), void * argument, unsigned createflag, unsigned *thrdaddr ){ _ptiddata ptd; /* pointer to per-thread data */ uintptr_t thdl; /* thread handle */ unsigned long err = 0L; /* Return from GetLastError() */ unsigned dummyid; /* dummy returned thread ID */ /* validation section */ _VALIDATE_RETURN(initialcode != NULL, EINVAL, 0); /* Initialize FlsGetValue function pointer */ __set_flsgetvalue(); /* * Allocate and initialize a per-thread data structure for the to- * be-created thread. */ if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL ) goto error_return; /* * Initialize the per-thread data */ _initptd(ptd, _getptd()->ptlocinfo); ptd->_initaddr = (void *) initialcode; ptd->_initarg = argument; ptd->_thandle = (uintptr_t)(-1);#if defined (_M_CEE) || defined (MRTDLL) if(!_getdomain(&(ptd->__initDomain))) { goto error_return; }#endif /* defined (_M_CEE) || defined (MRTDLL) */ /* * Make sure non-NULL thrdaddr is passed to CreateThread */ if ( thrdaddr == NULL ) thrdaddr = &dummyid; /* * Create the new thread using the parameters supplied by the caller. */ if ( (thdl = (uintptr_t) CreateThread( (LPSECURITY_ATTRIBUTES)security, stacksize, _threadstartex, (LPVOID)ptd, createflag, (LPDWORD)thrdaddr)) == (uintptr_t)0 ) { err = GetLastError(); goto error_return; } /* * Good return */ return(thdl); /* * Error return */error_return: /* * Either ptd is NULL, or it points to the no-longer-necessary block * calloc-ed for the _tiddata struct which should now be freed up. */ _free_crt(ptd); /* * Map the error, if necessary. * * Note: this routine returns 0 for failure, just like the Win32 * API CreateThread, but _beginthread() returns -1 for failure. */ if ( err != 0L ) _dosmaperr(err); return( (uintptr_t)0 );}
其中被標紅加粗的二部分是重點,即首先初始化了一個線程資料區塊(_ptiddata ptd),將線程函數地址及參數設定到線程資料區塊內,該塊是分配在堆上的。之後調用CreateThread函數建立線程,要注意傳入該函數的參數,即要啟動並執行函數_threadstartex(注意不是線程函數), 其參數是線程資料區塊(LPVOID)ptd
_threadstartex的功能是
1.將建立線程與記憶體資料區塊關聯(__fls_setvalue,該函數是作業系統函數,即所謂的線程局部儲存(Thread Local Storage, TLS))
2.調用_callthreadstartex來執行及終結真正的線程函數
static unsigned long WINAPI _threadstartex ( void * ptd ){ _ptiddata _ptd; /* pointer to per-thread data */ /* Initialize FlsGetValue function pointer */ __set_flsgetvalue(); /* * Check if ptd is initialised during THREAD_ATTACH call to dll mains */ if ( ( _ptd = (_ptiddata)__fls_getvalue(__get_flsindex())) == NULL) { /* * Stash the pointer to the per-thread data stucture in TLS */ if ( !__fls_setvalue(__get_flsindex(), ptd) ) ExitThread(GetLastError()); /* * Set the thread ID field -- parent thread cannot set it after * CreateThread() returns since the child thread might have run * to completion and already freed its per-thread data block! */ ((_ptiddata) ptd)->_tid = GetCurrentThreadId(); } else { _ptd->_initaddr = ((_ptiddata) ptd)->_initaddr; _ptd->_initarg = ((_ptiddata) ptd)->_initarg; _ptd->_thandle = ((_ptiddata) ptd)->_thandle;#if defined (_M_CEE) || defined (MRTDLL) _ptd->__initDomain=((_ptiddata) ptd)->__initDomain;#endif /* defined (_M_CEE) || defined (MRTDLL) */ _freefls(ptd); ptd = _ptd; } /* * Call fp initialization, if necessary */#ifndef MRTDLL#ifdef CRTDLL _fpclear();#else /* CRTDLL */ if (_FPmtinit != NULL && _IsNonwritableInCurrentImage((PBYTE)&_FPmtinit)) { (*_FPmtinit)(); }#endif /* CRTDLL */#endif /* MRTDLL */#if defined (_M_CEE) || defined (MRTDLL) DWORD domain=0; if(!_getdomain(&domain)) { ExitThread(0); } if(domain!=_ptd->__initDomain) { /* need to transition to caller‘s domain and startup there*/ ::msclr::call_in_appdomain(_ptd->__initDomain, _callthreadstartex); return 0L; }#endif /* defined (_M_CEE) || defined (MRTDLL) */ _callthreadstartex(); /* * Never executed! */ return(0L);}
static void _callthreadstartex(void){ _ptiddata ptd; /* pointer to thread‘s _tiddata struct */ /* must always exist at this point */ ptd = _getptd(); /* * Guard call to user code with a _try - _except statement to * implement runtime errors and signal support */ __try { _endthreadex ( ( (unsigned (__CLR_OR_STD_CALL
*)(void *))(((_ptiddata)ptd)->_initaddr) ) ( ((_ptiddata)ptd)->
_initarg ) ) ; } __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) ) { /* * Should never reach here */ _exit( GetExceptionCode() ); } /* end of _try - _except */}
_callthreadstartex函數功能如下:
1. 如上標紅地方運行真正線程函數
2.將真正線程函數運行完的傳回值作為傳回碼傳遞給_endthreadex結束該線程
至此,_beginthreadex就運行完畢了。
這裡_callthreadstartex調用_endthreadex直接刪除線程,而不是回退到_threadstartex,再到RtlUserThreadStart, 若直接返回的話,線程資料區塊並未刪除,會造成記憶體泄露。
總結下_beginthreadex的運行過程
1.先產生並初始化_tiddata記憶體塊,將線程函數地址及參數傳遞進去
2.調用CreateThread產生線程,運用RtlUserThreadStart函數運行線程函數(但要啟動並執行函數為_threadstartex,參數為線程資料區塊地址)
3._threadstartex將通過線程局部儲存(TLS)將線程資料區塊與運行線程綁定
4._threadstartex調用_callthreadstartex,運行真正的線程函數,當真正線程函數正確返回後用_endthreadex結束;若出錯,返回0;
下面附上_tiddata的具體內容,可以參考下(VS2008,C:\Program Files\Microsoft Visual Studio 9.0\VC\crt\src\mtdll.h)。
View Code三、線程終結
上文文中_callthreadstartex函數用_endthreadex來終結線程,源碼如下:
void __cdecl _endthreadex ( unsigned retcode ){ _ptiddata ptd; /* pointer to thread‘s _tiddata struct */ /* * Call fp termination, if necessary */#ifdef CRTDLL _fpclear();#else /* CRTDLL */ if (_FPmtterm != NULL && _IsNonwritableInCurrentImage((PBYTE)&_FPmtterm)) { (*_FPmtterm)(); }#endif /* CRTDLL */ ptd = _getptd_noexit(); if (ptd) { /* * Free up the _tiddata structure & its subordinate buffers * _freeptd() will also clear the value for this thread * of the FLS variable __flsindex. */ _freeptd(ptd); } /* * Terminate the thread */ ExitThread(retcode);}
所以,_endthreadex的功能如下:
1.刪除與該線程相關的線程資料區塊
2.調用ExitThread(與CreateThread相對)終結並傳遞結束代碼
參考:
<<Windows核心編程 第五版>>
Windows via C/C++ 中“RtlUserThreadStart函數”的翻譯
_beginThreadex建立多線程解讀
VC源碼:C:\Program Files\Microsoft Visual Studio 9.0\VC\crt\src\mtdll.h || threadex.c(VS2008)
http://blog.csdn.net/flyingleo1981/article/details/52788150
Windows線程生滅(圖文並茂)