天天看點

秒殺多線程第二篇---CreateThread與_beginthreadex本質差別

本文将帶領你與多線程作第一次親密接觸,并深入分析

CreateThread

_beginthreadex

的本質差別,相信閱讀本文後你能輕松的使用多線程并能流暢準确的回答

CreateThread

_beginthreadex

到底有什麼差別,在實際的程式設計中到底應該使用

CreateThread

還是

_beginthreadex

使用多線程其實是非常容易的,下面這個程式的主線程會建立了一個子線程并等待其運作完畢,子線程就輸出它的線程ID号然後輸出一句經典名言——Hello World。整個程式的代碼非常簡短,隻有區區幾行。

//最簡單的建立多線程執行個體
#include <stdio.h>
#include <windows.h>
//子線程函數
DWORD WINAPI ThreadFun(LPVOID pM)
{
    printf("子線程的線程ID号為:%d\n子線程輸出Hello World\n", GetCurrentThreadId());
    return ;
}
//主函數,所謂主函數其實就是主線程執行的函數。
int main()
{
    printf("--最簡單的建立多線程執行個體--\n");
    printf(" -- WILL for study --\n\n");

    HANDLE handle = CreateThread(NULL, , ThreadFun, NULL, , NULL);
    WaitForSingleObject(handle, INFINITE);
    return ;
}
           

結果:

--最簡單的建立多線程執行個體--
 -- WILL for study --

子線程的線程ID号為:2984
子線程輸出Hello World

Process returned 0 (0x0)   execution time : 0.217 s
Press any key to continue.
           

下面來細講下代碼中的一些函數

1、 CreateThread

函數功能:建立線程

函數原型:

HANDLE WINAPI CreateThread(

  LPSECURITY_ATTRIBUTES lpThreadAttributes,

  SIZE_T dwStackSize,

  LPTHREAD_START_ROUTINE lpStartAddress,

  LPVOID lpParameter,

  DWORD dwCreationFlags,

  LPDWORD lpThreadId

);
           

函數說明:

第一個參數表示線程核心對象的安全屬性,一般傳入NULL表示使用預設設定。

第二個參數表示線程棧空間大小。傳入0表示使用預設大小(1MB)。

第三個參數表示新線程所執行的線程函數位址,多個線程可以使用同一個函數位址。

第四個參數是傳給線程函數的參數。

第五個參數指定額外的标志來控制線程的建立,為0表示線程建立之後立即就可以進行排程,如果為

CREATE_SUSPENDED

則表示線程建立後暫停運作,這樣它就無法排程,直到調用

ResumeThread()

第六個參數将傳回線程的ID号,傳入NULL表示不需要傳回該線程ID号。

函數傳回值:

成功傳回新線程的句柄,失敗傳回NULL。

2、WaitForSingleObject

函數功能:等待函數 – 使線程進入等待狀态,直到指定的核心對象被觸發。

函數原形:

DWORD WINAPI WaitForSingleObject(

  HANDLE hHandle,

  DWORD dwMilliseconds

);
           

函數說明:

第一個參數為要等待的核心對象。

第二個參數為最長等待的時間,以毫秒為機關,如傳入5000就表示5秒,傳入0就立即傳回,傳入INFINITE表示無限等待。

因為線程的句柄線上程運作時是未觸發的,線程結束運作,句柄處于觸發狀态。是以可以用WaitForSingleObject()來等待一個線程結束運作。

函數傳回值:

在指定的時間内對象被觸發,函數傳回

WAIT_OBJECT_0

。超過最長等待時間對象仍未被觸發傳回

WAIT_TIMEOUT

。傳入參數有錯誤将傳回

WAIT_FAILED

CreateThread()

函數是Windows提供的API接口,在C/C++語言另有一個建立線程的函數

_beginthreadex()

,在很多書上(包括《Windows核心程式設計》)提到過盡量使用

_beginthreadex()

來代替使用

CreateThread()

,這是為什麼?下面就來探索與發現它們的差別吧。

首先要從标準C運作庫與多線程的沖突說起,标準C運作庫在1970年被實作了,由于當時沒任何一個作業系統提供對多線程的支援。是以編寫标準C運作庫的程式員根本沒考慮多線程程式使用标準C運作庫的情況。比如标準C運作庫的全局變量errno。很多運作庫中的函數在出錯時會将錯誤代号指派給這個全局變量,這樣可以友善調試。但如果有這樣的一個代碼片段:

if (system("notepad.exe readme.txt") == -)
{
    switch(errno)
    {
        ...//錯誤處理代碼
    }
}
           

假設某個線程A在執行上面的代碼,該線程在調用system()之後且尚未調用switch()語句時另外一個線程B啟動了,這個線程B也調用了标準C運作庫的函數,不幸的是這個函數執行出錯了并将錯誤代号寫入全局變量errno中。這樣線程A一旦開始執行switch()語句時,它将通路一個被B線程改動了的errno。這種情況必須要加以避免!因為不單單是這一個變量會出問題,其它像strerror()、strtok()、tmpnam()、gmtime()、asctime()等函數也會遇到這種由多個線程通路修改導緻的資料覆寫問題。

為了解決這個問題,Windows作業系統提供了這樣的一種解決方案——每個線程都将擁有自己專用的一塊記憶體區域來供标準C運作庫中所有有需要的函數使用。而且這塊記憶體區域的建立就是由C/C++運作庫函數

_beginthreadex()

來負責的。下面列出

_beginthreadex()

函數的源代碼(我在這份代碼中增加了一些注釋)以便讀者更好的了解

_beginthreadex()

函數與

CreateThread()

函數的差別。

//_beginthreadex源碼整理By MoreWindows( http://blog.csdn.net/MoreWindows )
_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 見注1
    uintptr_t thdl;         //thread handle 線程句柄
    unsigned long err = L; //Return from GetLastError()
    unsigned dummyid;    //dummy returned thread ID 線程ID号

    // validation section 檢查initialcode是否為NULL
    _VALIDATE_RETURN(initialcode != NULL, EINVAL, );

    //Initialize FlsGetValue function pointer
    __set_flsgetvalue();

    //Allocate and initialize a per-thread data structure for the to-be-created thread.
    //相當于new一個_tiddata結構,并賦給_ptiddata指針。
    if ( (ptd = (_ptiddata)_calloc_crt(, sizeof(struct _tiddata))) == NULL )
        goto error_return;

    // Initialize the per-thread data
    //初始化線程的_tiddata塊即CRT資料區域 見注2
    _initptd(ptd, _getptd()->ptlocinfo);

    //設定_tiddata結構中的其它資料,這樣這塊_tiddata塊就與線程聯系在一起了。
    ptd->_initaddr = (void *) initialcode; //線程函數位址
    ptd->_initarg = argument;              //傳入的線程參數
    ptd->_thandle = (uintptr_t)(-);

#if defined (_M_CEE) || defined (MRTDLL)
    if(!_getdomain(&(ptd->__initDomain))) //見注3
    {
        goto error_return;
    }
#endif  // defined (_M_CEE) || defined (MRTDLL)

    // Make sure non-NULL thrdaddr is passed to CreateThread
    if ( thrdaddr == NULL )//判斷是否需要傳回線程ID号
        thrdaddr = &dummyid;

    // Create the new thread using the parameters supplied by the caller.
    //_beginthreadex()最終還是會調用CreateThread()來向系統申請建立線程
    if ( (thdl = (uintptr_t)CreateThread(
                    (LPSECURITY_ATTRIBUTES)security,
                    stacksize,
                    _threadstartex,
                    (LPVOID)ptd,
                    createflag,
                    (LPDWORD)thrdaddr))
        == (uintptr_t) )
    {
        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.
    //回收由_calloc_crt()申請的_tiddata塊
    _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.
    //校正錯誤代号(可以調用GetLastError()得到錯誤代号)
    if ( err != L )
        _dosmaperr(err);
    return( (uintptr_t) ); //傳回值為NULL的效句柄
}
           

講解下部分代碼:

注1、

_ptiddata ptd

;中的

_ptiddata

是個結構體指針。在mtdll.h檔案被定義:

typedef struct _tiddata * _ptiddata
           

微軟對它的注釋為

Structure for each thread's data

。這是一個非常大的結構體,有很多成員。本文由于篇幅所限就不列出來了。

注2、

_initptd(ptd, _getptd()->ptlocinfo);

微軟對這一句代碼中的getptd()的說明為:

/* return address of per-thread CRT data */
_ptiddata __cdecl_getptd(void);
           

_initptd()

說明如下:

/* initialize a per-thread CRT data block */
void__cdecl_initptd(_Inout_ _ptiddata _Ptd,_In_opt_ pthreadlocinfo _Locale);
           

注釋中的CRT (C Runtime Library)即标準C運作庫。

注3、

if(!_getdomain(&(ptd->__initDomain)))

中的_getdomain()函數代碼可以在thread.c檔案中找到,其主要功能是初始化COM環境。

由上面的源代碼可知,

_beginthreadex()

函數在建立新線程時會配置設定并初始化一個

_tiddata

塊。這個

_tiddata

塊自然是用來存放一些需要線程獨享的資料。事實上新線程運作時會首先将

_tiddata

塊與自己進一步關聯起來。然後新線程調用标準C運作庫函數如strtok()時就會先取得

_tiddata

塊的位址再将需要保護的資料存入

_tiddata

塊中。這樣每個線程就隻會通路和修改自己的資料而不會去篡改其它線程的資料了。是以,如果在代碼中有使用标準C運作庫中的函數時,盡量使用

_beginthreadex()

來代替

CreateThread()

。相信閱讀到這裡時,你會對這句簡短的話有個非常深刻的印象,如果有面試官問起,你也可以流暢準确的回答了^_^。

接下來,類似于上面的程式用

CreateThread()

建立輸出“Hello World”的子線程,下面使用

_beginthreadex()

來建立多個子線程:

//建立多子個線程執行個體
#include <stdio.h>
#include <process.h>
#include <windows.h>
//子線程函數
unsigned int __stdcall ThreadFun(PVOID pM)
{
    printf("線程ID号為%4d的子線程說:Hello World\n", GetCurrentThreadId());
    return ;
}
//主函數,所謂主函數其實就是主線程執行的函數。
int main()
{
    printf("--最簡單的建立多線程執行個體--\n");
    printf(" -- WILL for study --\n\n");

    const int THREAD_NUM = ;
    HANDLE handle[THREAD_NUM];
    int i;
    for (i = ; i < THREAD_NUM; i++)
        handle[i] = (HANDLE)_beginthreadex(NULL, , ThreadFun, NULL, , NULL);
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
    return ;
}
           

結果:

--最簡單的建立多線程執行個體--
 -- WILL for study --

線程ID号為11004的子線程說:Hello World
線程ID号為10612的子線程說:Hello World
線程ID号為8240的子線程說:Hello World
線程ID号為4764的子線程說:Hello World
線程ID号為4708的子線程說:Hello World

Process returned 0 (0x0)   execution time : 0.231 s
Press any key to continue.
           

圖中每個子線程說的都是同一句話,不太好看。能不能來一個線程報數功能,即第一個子線程輸出1,第二個子線程輸出2,第三個子線程輸出3,……。要實作這個功能似乎非常簡單——每個子線程對一個全局變量進行遞增并輸出就可以了。代碼如下:

///子線程報數
#include <stdio.h>
#include <process.h>
#include <windows.h>
int g_nCount;
//子線程函數
unsigned int __stdcall ThreadFun(PVOID pM)
{
    g_nCount++;
    printf("線程ID号為%4d的子線程報數%d\n", GetCurrentThreadId(), g_nCount);
    return ;
}
//主函數,所謂主函數其實就是主線程執行的函數。
int main()
{
    printf("--最簡單的建立多線程執行個體--\n");
    printf(" -- WILL for study --\n\n");

    const int THREAD_NUM = ;
    HANDLE handle[THREAD_NUM];

    g_nCount = ;
    int i;
    for (i = ; i < THREAD_NUM; i++)
        handle[i] = (HANDLE)_beginthreadex(NULL, , ThreadFun, NULL, , NULL);
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
    return ;
}
           

結果:

--最簡單的建立多線程執行個體--
 -- WILL for study --

線程ID号為4772的子線程報數1
線程ID号為9328的子線程報數2
線程ID号為9872的子線程報數3
線程ID号為9912的子線程報數4
線程ID号為6400的子線程報數5
線程ID号為7560的子線程報數6
線程ID号為4188的子線程報數7
線程ID号為11204的子線程報數8
線程ID号為10884的子線程報數9
線程ID号為 700的子線程報數10

Process returned 0 (0x0)   execution time : 0.597 s
Press any key to continue.
           

顯示結果從1數到10,看起來好象沒有問題。

答案是不對的,雖然這種做法在邏輯上是正确的,但在多線程環境下這樣做是會産生嚴重的問題,下一篇《秒殺多線程第三篇 原子操作 Interlocked系列函數》将為你示範錯誤的結果(可能非常出人意料)并解釋産生這個結果的詳細原因。

轉載:http://blog.csdn.net/morewindows/article/details/7421759

繼續閱讀