天天看點

CreateThread、_beginthreadex、AfxBeginThread 詳解

在Windows的多線程程式設計中,建立線程的函數主要有CreateThread,_beginthead(_beginthreadex)和AfxBeginThread,那麼它們之間有什麼聯系與差別呢?當我需要建立一個線程時該用哪個函數呢?

一、各個函數的用法:

1. CreateThread:

函數原型:

HANDLE WINAPI CreateThread(

_in LPSECURITY_ATTRIBUTES lpThreadAttributes,

_in SIZE_T dwStackSize,

_in LPTHREAD_START_ROUTINE lpStartAddress,

_in LPVOID lpParameter,

_in DWORD dwCreationFlags,

_out LPDWORD lpThreadId 

);

參數:

        lpThreadAttributes: 指向一個LPSECURITY_ATTRIBUTES結構的指針決定傳回的句柄能否被繼承,如果lpThreadAttributes為空,這個句柄不能被繼承。

         sdStackSize:初始化的堆棧大小,以位元組為機關。如果為0,使用預設的大小。

         lpStartAddress:函數的入口位址,是一個函數指針。

                   函數原型:DWORD WINAPI ThreadProc( [in]  LPVOID  lpParameter);

         lpParameter:一個指針,被傳遞到線程函數裡。

         dwCreationFlags:線程建立的标志。如果為CREATE_SUSPENDED這個标志,那麼需要使用ResumeThread函數來激活線程函數,如果為NULL,線程函數立刻執行。

         IpThreadId:一個指向線程id的指針,如果為空,線程id不被傳回。

傳回值:

         1:如果函數成功執行,傳回值将是這個新線程的句柄。如果失敗,傳回值是NULL。

2:當線程函數的起始位址無效(或者不可通路)時,CreateThread函數仍可能成功傳回。如果該起始位址無效,則當線程運作時,異常将發生,線程終止,并傳回一個錯誤代碼,可以使用GetLastError擷取。

說明:

1:如果線程函數return,傳回值會隐式條用ExitThread函數,可以使用GetExitCodeThread函數獲得該線程函數的傳回值。

         2:使用CreateThread建立的線程具有THREAD_PRIORITY_NORMAL線程優先級。可以使用GetThreadPriority和SetThreadPriority函數擷取和設定線程優先級值。

         3:當一個線程結束時,這個線程的對象将獲得有信号狀态,使得任何等待這個對象的線程都能夠成功并繼續執行下去。

4:系統中的線程對象一直存活到線程結束,并且所有指向它的句柄都需要通過調用CloseHandle關閉。

5:如果一個線程調用了CRT,應該使用_beginthreadex 和_endthreadex(需要使用多線程版的CRT)。

2. _beginthread與_beginthreadex:

函數原型:

uintptr_t _beginthread(

   void( *start_address )( void * ),

   unsigned stack_size,

   void *arglist

);

uintptr_t _beginthreadex(

   void *security,

   unsigned stack_size,

   unsigned ( *start_address )( void * ),

   void *arglist,

   unsigned initflag,

   unsigned *thrdaddr

);      

參數:

         Start_address:線程函數的入口位址。對于_beginthread,線程函數的調用約定是_cdecl。對于_beginthreadex,線程函數的調用約定是_stdcall。

         stack_size:線程堆棧大小,可以為0。

         arglist:傳遞給線程函數的參數,可以為0。

         security:線程安全屬性。

         Initflag:線程建立的初始标志。為CREATE_SUSPENDED則挂起線程,使用ResumeThread激活線程,為NULL則立即執行。

         thrdaddr:線程Id。

傳回值:

1:如果成功,将會傳回一個新的線程句柄。然而,如果線程函數執行的很快,_beginthread可能得到一個非法的句柄。

2:如果失敗,_beginthread傳回-1,此時errno變量将被設定。_beginthreadex傳回0,此時errno和_doserrno都被設定。

說明:

1:_beginthread函數的線程入口函數必須使用_cdecl調用約定。_beginthreadex函數的線程入口函數必須使用_stdcall調用約定

2:使用_beginthreadex比使用_begingthread更加安全。因為_beginthread的線程函數可能執行很快,這時可能會傳回一個非法的句柄。

         3:_endthread将會自動的關閉線程句柄,然而_beginthreadex不會,需要使用CloseHandle現實的關閉句柄。是以_beginthreadex函數可以使用WaitForSingleObject函數來擷取線程對象來進行同步。

         4:一個連接配接Libcmt.lib的可執行檔案,不要調用ExitThread函數,這個函數會阻止系統的運作時回收已配置設定的資源。使用_endthread and _endthreadex可以回收已配置設定的資源然後再調用ExitThread.

5:可以調用_endthread和_endthreadex顯示式結束一個線程。然而,當線程函數傳回時,_endthread和_endthreadex 被自動調用。endthread和_endthreadex的調用有助于確定配置設定給線程的資源的合理回收。

6:當_beginthread和_beginthreadex被調用時,作業系統自己處理線程棧的配置設定。如果在調用這些函數時,指定棧大小為0,則作業系統為該線程建立和主線程大小一樣的棧。如果任何一個線程調用了abort、exit或者ExitProcess,則所有線程都将被終止。

7:對于使用C運作時庫裡的函數的線程應該使用_beginthread和_endthread這些C運作時函數來管理線程,而不是使用CreateThread和ExitThread。否則,當調用ExitThread後,可能引發記憶體洩露。

8:必須使用多線程版的 C run_time libraries.

3. AfxBeginThread:

函數原型:

CWinThread* AfxBeginThread(

   AFX_THREADPROC pfnThreadProc,

   LPVOID pParam,

   int nPriority = THREAD_PRIORITY_NORMAL,

   UINT nStackSize = 0,

   DWORD dwCreateFlags = 0,

   LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL

);

CWinThread* AfxBeginThread(

   CRuntimeClass* pThreadClass,

   int nPriority = THREAD_PRIORITY_NORMAL,

   UINT nStackSize = 0,

   DWORD dwCreateFlags = 0,

   LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL

);

參數:

pfnThreadProc:線程函數的入口位址。

函數原型:UINT __cdecl MyControllingFunction( LPVOID pParam );

         pThreadClass:繼承CWinThread類的RUNTIME_CLASS對象。

         pParam: 傳遞給線程函數的參數,可以為0。

         nPriority:線程優先級。

         nStackSize:指明線程堆棧的大小,以位元組為機關,可以為0。

         dwCreateFlags:線程建立标志。

         lpSecurityAttrs:線程安全屬性。

傳回值:

         如果成功則傳回一個指針指向線程對象,否則為NULL。

 說明:

         可以調用AfxEndThread來終止線程或者return。

二、各個函數的聯系與差別:

CreateThread:

         CreateThread是Windows的API函數,提供作業系統級别的建立線程的操作。_beginthread(及_beginthreadex)與AfxBeginThread的底層實作都調用了CreateThread函數。

         CreateThread函數沒有考慮到下面二點:

(1)C Runtime中需要對多線程進行記錄和初始化,以保證C函數庫工作正常(典型的例子就是strtok函數)

(2)MFC也需要知道新線程的建立,也需要做一些初始化工作。

是以,在不調用MFC和CRT的函數時,可以用CreateThread建立線程,其它情況不要使用。

AfxBeginThread:

         MFC中線程建立的函數,首先建立了相應的CWinThread對象,然後調用CWinThread::CreateThread,在CWinThread::CreateThread中完成了對線程對象的初始化工作,然後,調用_beginthreadex建立線程。注意不要在一個MFC程式中使用_beginthreadex()或CreateThread()。

_beginthread和_beginthreadex: (實作檔案分别是thread.c和threadex.c)

         是MS對C Runtime庫的擴充SDK函數,首先對C Runtime庫做了一些初始化的工作,以保證C Runtime庫工作正常。然後,調用CreateThread真正建立線程。

若要使多線程C和C++程式能夠正确地運作,必須建立一個資料結構,并将它與使用C/C++運作期庫函數的每個線程關聯起來。當你調用C/C++運作期庫時,這些函數必須知道檢視調用線程的資料塊,這樣就不會對别的線程産生不良影響。

1.每個線程均獲得由C/C++運作期庫的堆棧配置設定的自己的tiddata記憶體結構。  

2.傳遞給_beginthreadex的線程函數的位址儲存在tiddata記憶體塊中。傳遞給該函數的參數也儲存在該資料塊中。(指向tiddata結構的指針會作為一個TLS儲存起來)   

3._beginthreadex确實從内部調用CreateThread,因為這是作業系統了解如何建立新線程的唯一方法。

4.當調用CreatetThread時,它被告知通過調用_threadstartex而不是pfnStartAddr來啟動執行新線程。還有,傳遞給線程函數的參數是tiddata結構而不是pvParam的位址。

5.如果一切順利,就會像CreateThread那樣傳回線程句柄。如果任何操作失敗了,便傳回 NULL。

三、常見問題:

1:《Win32多線程程式設計》第219頁中間:不要在一個MFC程式中使用_beginthreadex()或CreateThread()。

    我的程式中使用的CString/CArray等MFC類,是否我隻能使用AfxBeginThread()?

2:我的程式使用了一個庫,裡面用到了很多memcpy/memmove/fopen/fread等函數。

    這些函數應該算是C Run-time Library中的函數吧?

    我建立的線程中調用了這個庫中的函數,是不是使用_beginthreadex()才是安全的?

3:《Win32多線程程式設計》第228頁中間:如果主線程以下的任何線程進行了以下操作,你就應該使用多線程版的C runtime library,并使用_beginthreadex()和_endthreadex():在C++程式中使用了new和delete。

    我的問題是,在C++程式中,幾乎都要用到new和delete,難道隻有使用_beginthreadex()嗎?

---------------------------------------------------------------

1、不要在一個MFC程式中使用_beginthreadex()或CreateThread()。

這句話的意思是由于AfxBeginThread()是MFC封裝的啟動線程的函數,裡面包含了很多和MFC相關的啟動資訊,而且封裝了一些常用的操作,使用起來也比較簡便。而用另外兩個函數就需要程式員對類型,安全性檢查進行更多的思考!

2、用_beginthreadex()函數應該是最佳選擇,因為_beginthreadex()函數是C Run-time Library 中的函數,函數的參數和資料類型都是C Run-time Library中的類型,這樣在啟動線程時就不需要進行Windows資料類型和C Run-time Library中的資料類型之間的轉化。減低了線程啟動時的資源消耗和時間的消耗!

3、在C++程式中,幾乎都要用到new和delete,難道隻有使用_beginthreadex()?

不,因為MFC也是C++類庫(隻不過是Microsoft的C++類庫,不是标準的C++類庫),在MFC中也封裝了new和delete兩中運算符,是以用到new和delete的地方不一定非要使用_beginthreadex() 函數,用其他兩個函數都可以!

其實在程式中使用上面的哪個函數并不是絕對的,書的作者隻不過是提了一個更佳的搭配方法,我在

MFC程式中也經常使用_beginthreadex()和CreateThread()這兩個函數,運作的效果也沒有多大的差別,有的時候隻是需要你額外的進行一些類型檢查和其他的一些轉化操作,其餘沒有其他不妥!

上面是俺的一些了解,和在工作中的一點感覺!

四、總結:

1:CreateThread是由作業系統提供的接口,而AfxBeginThread和_BeginThread則是編譯器對它的封裝。

2:用_beginthreadex()、_endthreadex函數應該是最佳選擇,且都是C Run-time Library中的函數,函數的參數和資料類型都是C Run-time Library中的類型,這樣在啟動線程時就不需要進行Windows資料類型和C Run-time Library中的資料類型之間的轉化,進而減低了線程啟動時的資源消耗和時間的消耗。

3:MFC也是C++類庫(隻不過是Microsoft的C++類庫,不是标準的C++類庫),在MFC中也封裝了new和delete兩中運算符,是以用到new和delete的地方不一定非要使用_beginthreadex() 函數,用其他兩個函數都可以。

4:_beginthreadex和_beginthread在回調入口函數之前進行一些線程相關的CRT的初始化操作。CRT的函數庫線上程出現之前就已經存在,是以原有的CRT不能真正支援線程,這也導緻了許多CRT的函數在多線程的情況下必須有特殊的支援,不能簡單的使CreateThread就可以。

5:如果要作多線程(非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。

6:你也許會借助win32來處理記憶體配置設定和io,這時候你确實可以以單線程crt配合CreateThread,因為io的重任已經從crt轉交給了win32。這時通常你應該使用HeapAlloc,HeapFree來處理記憶體配置設定,用CreateFile或者GetStdHandle來處理io。

7:還有一點比較重要的是_beginthreadex傳回的雖然是個unsigned long,其實是個線程Handle(事實上_beginthreadex在内部就是調用了CreateThread),是以你應該用CloseHandle來結束他。千萬不要使用ExitThread()來退出_beginthreadex建立的線程,那樣會喪失釋放簿記資料的機會,應該使用_endthreadex.

下面對兩個概念進行闡述

CRT(C/C++ Runtime Library):

是一種函數庫,由編譯器的生産廠家提供頭檔案或接口,作業系統提供運作時庫的實作。是以Windows和Linux系統的運作時庫函數接口雖然一樣,但具體實作不一樣。

CRT是支援C/C++運作的一系列函數和代碼的總稱,雖然沒有一個很精确的定義,但是可以知道,你的main函數就是它負責調用的,還有平時使用的strlen,strtok,time,atoi之類的函數也是它提供的。

線程局部存儲(TLS,thread local storage)

         一個多線程程式中,全局變量(及配置設定的記憶體)被所有線程所共享。函數的靜态局部變量也被所有使用該函數的線程所共享。一個函數中的自動變量對每一個線程是唯一的,因為它們存儲于堆棧上,而每個線程都有他們自己的堆棧。有時,我們需要對每一個線程唯一的持續性存儲。例如,C函數strtok就需要這種存儲。不幸的是,C語言不支援這種變量。但是Windows提供了四個API函數來實作這種機制。我們把這種存儲稱為線程局部存儲(TLS,Thread Local Storage)。

首先,定義一個結構,把對每個線程唯一的資料包含在該結構中。

例如:

 typedef struct

 {        int one;        int two;   

} DATA, *PDATA;

然後,主線程調用TlsAlloc函數來為程序獲得一個TLS索引:tlsIndex = TlsAlloc();該TLS索引可以存儲于一個全局變量或者通過線程函數的參數傳遞給其它線程。每個需要使用該TLS索引的線程,先動态配置設定記憶體,然後調用TlsSetValue函數将該記憶體關聯到該TLS索引(及該線程): TlsSetValue(tlsIndex, GlobalAlloc(GPTR, sizeof(DATA));

         此時,線程直接或間接調用的函數可以通過如下方式獲得該線程的TLS存儲區域:

        PDATA pdata;

        pdata = (PDATA) TlsGetValue(tlsIndex);

此時,就可以使用該線程的TLS存儲區的變量了。

 當線程函數終止時,它應該釋放它所配置設定的動态空間: GlobalFree(TlsGetValue(tlsIndex));

 當所有使用TLS的線程都終止後,主線程應當釋放該TLS存儲空間: TlsFree(tlsIndex);

    TLS可以以一種更簡單的方式使用,那就是通過Winodws對C所作的擴充關鍵字__declspec和擴充存儲類型修飾符thread。例如:

    __declspec(thread) int global_tls_i = 1;        // 在函數外部,聲明一個TLS變量

    __declspec(thread) static int local_tls_i = 2;  // 在函數内部聲明一個靜态TLS變量

繼續閱讀