Windows線程創建、退出及資源釋放

可以通過以下幾種方法創建一個線程:
1、CreateThread
2、_beginthread
3、_beginthreadex
4、AfxBeginThread


1、CreateThread

函數原型

HANDLE CreateThread(LPSECURITY_ATTRIBUTES                lpThreadAttributes,//SD
        SIZE_T dwStackSize, // initial stack size
        LPTHREAD_START_ROUTINE lpStartAddress, // thread function
        LPVOID lpParameter, // thread argument
        DWORD dwCreationFlags, // creation option
        LPDWORD lpThreadId // thread identifier
);

參數:

lpThreadAttributes:指向SECURITY_ATTRIBUTES型態的結構的指針。在Windows 98中忽略該參數。在Windows NT中,NULL使用默認安全性,不可以被子線程繼承,否則需要定義一個結構體將它的bInheritHandle成員初始化爲TRUE
dwStackSize:設置初始棧的大小,以字節爲單位,如果爲0,那麼默認將使用與調用該函數的線程相同的棧空間大小。任何情況下,Windows根據需要動態延長堆棧的大小。
lpStartAddress:指向線程函數的指針,必須以下列形式聲明:

DWORD WINAPI ThreadProc (LPVOID lpParam) ,格式不正確將無法調用成功。

//也可以直接調用void類型
//但lpStartAddress要這樣通過LPTHREAD_START_ROUTINE轉換如:(LPTHREAD_START_ROUTINE)MyVoid
//然後在線程聲明爲:
void MyVoid()
{
    return;
}

lpParameter:向線程函數傳遞的參數,是一個指向結構的指針,不需傳遞參數時,爲NULL。
dwCreationFlags :線程標誌,可取值如下:


CREATE_SUSPENDED(0x00000004):創建一個掛起的線程,
0:表示創建後立即激活。
STACK_SIZE_PARAM_IS_A_RESERVATION(0x00010000):**dwStackSize參數指定初始的保留堆棧的大小,否則,dwStackSize指定提交的大小。該標記值在Windows 2000/NT and Windows Me/98/95上不支持。


lpThreadId:保存新線程的id。

返回值:

函數成功,返回線程句柄;函數失敗返回false。若不想返回線程ID,設置值爲NULL。
實例:

PMYDATA pData;

pData = (PMYDATA)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
sizeof(MYDATA));
if( pData == NULL )
ExitProcess(2);
// Generate unique data for each thread.
pData->val1 = i;
pData->val2 = i+100;

HANDLE hThread = CreateThread(  NULL, // default security attributes
            0, // use default stack size
            ThreadProc, // thread function
            pData, // argument to thread function
            0, // use default creation flags
            &dwThreadId[i]); // returns the thread identifier
// Check the return value for success.
if (hThread == NULL)
{
    ExitProcess(i);
}

2、_beginthread

函數原型:

uintptr_t _beginthread( void( *start_address )( void * ),
            unsigned stack_size,
            void *arglist 
);

參數:

start_address:新線程的起始地址 ,指向新線程調用的函數的起始地址
stack_size:新線程的堆棧大小,可以爲0
arglist: 傳遞給線程的參數列表,無參數是爲NULL

返回值:

假如成功,函數將返回一個處理信息對這個新創建的線程。如果失敗_beginthread將返回-1。
引用頭文件:

#include <process.h>

實例:

m_hThreadHandle = HANDLE(_beginthread(CAppLog::LogProcThread, 0, this));

3、_beginthreadex

函數原型(MSDN):

unsigned long _beginthreadex(   void *security, 
                unsigned stack_size, 
                unsigned ( __stdcall *start_address )( void * ), 
                void *arglist, 
                unsigned initflag, 
                unsigned *thrdaddr );

參數:

security:安全屬性,NULL爲默認安全屬性
stack_size:指定線程堆棧的大小。如果爲0,則線程堆棧大小和創建它的線程的相同。一般用0
start_address:指定線程函數的地址,也就是線程調用執行的函數地址(用函數名稱即可,函數名稱就表示地址)
arglist:傳遞給線程的參數的指針,可以通過傳入對象的指針,在線程函數中再轉化爲對應類的指針
initflag:線程初始狀態,0:立即運行;CREATE_SUSPEND:suspended(懸掛)
thrdaddr:用於記錄線程ID的地址

返回值:

與_beginthread()不同的是:_beginthread返回-1表示失敗, 而_beginthreadex()返回0表示失敗!

引用頭文件:

#include <process.h>

實例:

HANDLE hThread;

hThread = (HANDLE)_beginthreadex( NULL, 0, &SIPVoIPLink::ReceivingThrd, (LPVOID)this, 0, &threadID );

if(hThread == NULL) return false;

4、AfxBeginThread

這個看起來就直接了,MFC封裝的函數。
MFC中,用戶界面線程工作者線程都是由AfxBeginThread創建的,MFC提供了兩個重載版的AfxBeginThread,一個用於用戶界面線程,另一個用於工作者線程,分別有如下的原型和過程:

原型一(用戶界面線程):

CWinThread* AFXAPI AfxBeginThread(  CRuntimeClass* pThreadClass,
                    int nPriority,
                    UINT nStackSize,
                    DWORD dwCreateFlags,
                    LPSECURITY_ATTRIBUTES lpSecurityAttrs)

參數:

pThreadClass:從CWinThread派生的RUNTIME_CLASS類;
nPriority:指定線程優先級,如果爲0,則與創建該線程的線程相同;
nStackSize:指定線程的堆棧大小,如果爲0,則與創建該線程的線程相同;
dwCreateFlags:一個創建標識,如果是CREATE_SUSPENDED,則在懸掛狀態創建線程,在線程創建後線程掛起,否則線程在創建後開始線程的執行。
lpSecurityAttrs:表示線程的安全屬性,NT下有用。
原型二(工作線程)

CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc,
                LPVOID lParam,
                int nPriority = THREAD_PRIORITY_NORMAL,
                UINT nStackSize = 0,
                DWORD dwCreateFlags = 0,
                LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
                );//用於創建工作者線程

參數:

pfnThreadProc : 線程的入口函數,聲明一定要如下: UINT MyThreadFunction(LPVOID pParam),不能設置爲NULL;
pParam : 傳遞入線程的參數,注意它的類型爲:LPVOID,所以我們可以傳遞一個結構體入線程.
nPriority : 線程的優先級,一般設置爲 0 .讓它和主線程具有共同的優先級.
nStackSize : 指定新創建的線程的棧的大小.如果爲 0,新創建的線程具有和主線程一樣的大小的棧
dwCreateFlags : 指定創建線程以後,線程有怎麼樣的標誌.可以指定兩個值:


CREATE_SUSPENDED : 線程創建以後,會處於掛起狀態,直到調用:ResumeThread
0 : 創建線程後就開始運行.


lpSecurityAttrs : 指向一個 SECURITY_ATTRIBUTES 的結構體,用它來標誌新創建線程的安全性.如果爲 NULL,

返回值:

一個指向新線程的線程對象的指針

以上幾種創建線程的區別

其實windows只提供了一個創建線程的方法,就是CreateThread,後面三個函數都是由CreateThread間接得到。
1、_beginthread和_beginthreadex的區別

首先我們看看這兩個函數都幹了什麼:

uintptr_r __cdecl _beginthreadex(...)
{
    //爲即將創建的線程分配一個數據結構_ptiddata ptd(per-thread data)
    //初始化這個數據結構,其中ptd->_thandle = (uintptr_t)(-1)
    //如果初始化失敗,返回(uintptr_t)(0) [_beginthread返回-1]
    //用傳進來的參數,調用CreateThread
    //如果創建成功返回CreateThread返回的代碼
    //如果創建失敗則釋放ptd,並返回(uintptr_t)(0) [_beginthread返回-1,而CreateThread失敗返回0,非-1]
}

然後再看看這兩個函數有什麼不同
(1)參數列表不同, ex版本的參數和CreateThread差不多:
(2)二者在初始化ptd失敗時返回的值不同
(3)_beginthread的參數缺少安全描述符. 而且它是創建線程的時候先以掛起狀態創建 (CreateThread會填充ptd->_thandle和ptd->_tid) 然後再ResumeThread。_beginthread是根據傳進來的參數創建線程
(4)失敗返回值不同,ex版本的與Windows API CreateThread返回值是一直的,這也是提倡使用後者的原因之一

2、CreateThread()、_beginthreadex()及、AfxBeginThread()

CreateThread時Windows API接口函數,_beginthreadex函數是C/C++運行庫提供的函數,從 _beginthreadex函數的源代碼,可以看出它的主要動作是:增加了一個名爲ptd的_ptiddata的結構的處理,然後在調用CreateThread函數。_ptiddata是每個線程都擁有自己的專用的數據結構。
AfxBeginThread是MFC封裝的啓動線程的函數,裏面包含了很多和MFC相關的啓動信息,而且封裝了一些常用的操作,使用起來也比較簡便。而用另外兩個函數就需要程序員對類型,安全性檢查進行更多的思考!


線程退出方式

1.線程函數返回
2.線程通過調用ExitThread終止自己(自己調用)
3.TerminateThread(本進程或其它進程的線程調用)
4.包含線程的進程終止

第一種是最完美的退出方式,應儘可能使用第一種方法以避免資源泄漏!


線程資源釋放

必須明白,一個線程終止和線程資源釋放那個不是一個概念。當一個線程以上面幾種退出方式終止之後,線程對象及資源還存在於內存中,所以在線程終止之後還需要某種方式釋放資源。

線程的釋放方式與創建方式相關聯:
CreateThread創建的線程應使用CloseHandle關閉。
_beginthread創建的線程應使用_endthread關閉。
_beginthreadex創建的線程應使用_endthreadex關閉。
AfxBeginThread創建的線程,如果是線程自己結束自己的線程(從CWinThread繼承出來的)就用AfxEndThread。如果外部調用的話可以用PostThreadMessage(m_nThreadID, WM_QUIT,0,0);給這個線程發送消息,線程就會結束的,其中的m_nThreadID是線程ID。

線程資源的釋放方法(常用):

1、如果對創建線程的引用不感興趣,可在創建之後直接關閉句柄

void main()
{
    ……
    HANDLE hThread = CreateThread(NULL,0,ThreadFunc,NULL,0,NULL);
    CloseHandle(hThread);
    ……
}

有些新手可能會有個疑問,爲什麼在剛創建完線程之後就把句柄關掉了?
要特別注意句柄和實例區別,句柄只是對實例對象的引用,用以操作對象。我們使用CreateThread創建了一個線程實例(可將線程整體看作一個對象),返回的線程句柄只是提供了一個方法(類似於指針)讓我們可以訪問或操作線程對象。我們這裏只需要讓創建的線程執行必要的代碼段就好了,之後並不需要在對其進行任何操作,所以可以直接將其CloseHandle掉,關閉句柄只是切斷了訪問線程的方式罷了,線程還是存在並運行着的。

然而,只是這樣解釋還是有所偏差,因爲在很多使用CloseHandle關閉其他對象句柄的操作中都會釋放對象佔用的資源,而對於線程,在調用CloseHandle之後並不會終止線程,也就不會立馬釋放線程資源(實現中線程還必須繼續運行),調用CloseHandle之後系統會遞減線程內核對象的使用計數,當線程執行完畢(線程函數執行完)之後也會遞減此線程內核對象使用計數,當計數爲0時纔會釋放線程資源!
反過來看,如果不使用CloseHandle關閉線程句柄,那麼系統就會一直保持着對此線程內核對象的引用,這樣,即使線程執行完畢,使用計數也不會爲0,所以線程資源不會被釋放。
因此,CloseHandle關閉線程句柄是釋放線程資源的必要途徑,但不會影響線程的正常運行!所以CloseHandle的調用位置非常靈活,即可在線程剛創建出來之後緊接着調用,也可以在線程執行完之後才調用!

2、外部線程WaitForSingleObject,發現線程中止運行後,釋放線程相關的資源

void main()
{
    CThread thread; //一個自己定義的線程封裝類,其中有一些成員變量,Start啓動線程函數,Release釋放資源函數。
    thread.Start();
    while(WaitForSingleObject(thread.m_hThread,1000) != WAIT_OBJECT_0)
    {
        //TerminateThread(thread.m_hThread);//在這裏如果你不想再等待,可以中止它
        Sleep(1000);
    }
    thread.Release();
}

這種方式的優點是對線程的全面的管理。
缺陷在於,如果要即時釋放資源,必須有一個專門的外部的線程來不斷的監視或管理線程運行狀態。

3、線程退出時將自身資源釋放

DWORD CALLBACK CThread::Thread(VOID *pParam) //線程的入口函數,static函數
{
    CThread *pthread = pParam;
    ... //工作狀態設置
    pthread->Execute()
    ... //工作狀態設置
    pthread->Release() //在所有的工作做完之後進行釋放資源
    return 0;
}

這種方式的優點是對線程的資源的時機是最準確的,缺陷是必須要保證線程在需要退出時不會阻塞。

3、在實際的工作中,我們需要這多種方式的集合來實現我們的功能

如有個網絡服務器,它給每個用戶的新建一個線程來響應它的請求,如果用戶退出,要保證相關資源要完整的釋放,並且服務器本身有停止功能,停止時,不論有多少和用戶交互,都必須中止所有線程,且釋放所有資源。
要解決的子問題:
Q1:用戶退出時資源釋放:
解決方式:
(1)使用線程自身退出時釋放的方式。
(2)使用一個外部監視線程,發現線程退出時釋放相應的資源。

Q2:服務器停止時退出且釋放所有線程:
設用戶線程工作狀態爲停止,等待用戶線程自然退出,如果沒有退出,可能是阻塞狀態。這時使用強制退出的方法。
釋放所有用戶線程相關的資源,可以根據資源的使用時間和方式(如某些Windows資源句柄),在線程退出之前的某時機進行釋放。

下面是我實作的滿足以上要求的自身退出方式釋放資源的線程類的代碼片段:

int CThread::Stop()
{
    m_RunningMutex.Lock(); //判斷線程的工作狀態,如果已經退出,直接返回
    if(!m_bRunning)
    {
        m_RunningMutex.Unlock();
        return ERR_VTHREAD_ALREAD_STOP;
    }
    m_RunningMutex.Unlock();

    m_StopMutex.Lock();
    m_bStop = True;  //設置線程的工作狀態爲停止
    m_StopMutex.Unlock();
#ifdef _WIN32
    DWORD ThreadId = GetCurrentThreadId(); //判斷是否線程自身退出,如果是,則返回,不能調用強制退出的方法
#else
    pthread_t ThreadId = pthread_self();
#endif
     if(ThreadId != m_ThreadID)
    {
        m_dwExitMode = EXIT_BY_OTHER;
    }
    else
    {
        m_dwExitMode = EXIT_BY_SELF;
    }

 if(m_dwExitMode == EXIT_BY_OTHER) //如果是服務器停止信號
 {
    int nTryTime = 0;
    bool bStopTry = false;
    while( m_bRunning && !bStopTry) //嘗試判斷線程是否停止,最好使線程自然退出
    {
        if (nTryTime > m_nStopWaitTime) bStopTry = true;
        Sleep(10000);
        nTryTime ++;
    }
    if(bStopTry) //如果線程沒有退出,就強制退出
#ifdef _WIN32
        TerminateThread(m_hThread, DEF_EXIT_CODE);
#else
        pthread_cancel(m_ThreadID);
#endif
        m_RunningMutex.Lock();
        m_bRunning = False;
        m_RunningMutex.Unlock();

        Release(); //線程已經停止,釋放所有私有次源,這行代碼可以在服務器端,讓此函數只執行停止功能
    }
    return 0;
}
//這是線程的主函數,它包含的釋放自身私有資源的功能。
#ifdef WIN32
DWORD __stdcall CThread::ThreadProc( VOID *lpParameter )
#else
VOID* CThread::ThreadProc( VOID *lpParameter )
#endif
{
 BOOL bStop;
#ifndef _WIN32
    //pthread_detach(pthread_self());
#endif
    CThread* pThread = (CThread*)lpParameter;

    pThread->m_RunningMutex.Lock();
    pThread->m_bRunning = True;
    pThread->m_RunningMutex.Unlock();

    pThread->m_StopMutex.Lock();
    bStop = pThread->m_bStop;
    pThread->m_StopMutex.Unlock();
    if( !bStop)
    {
        pThread->Execute();
    }
    pThread->m_RunningMutex.Lock();
    pThread->m_bRunning = False;
    pThread->m_RunningMutex.Unlock();
    if(pThread->m_dwExitMode == EXIT_BY_SELF) //在這裏判斷線程是否自身退出,如是:釋放私有資源
    {
        pThread->Release();
     }
     //退出線程,使線程以最自然的方式退出.
    return 0;
}

深入討論幾種創建線程的方式

(1)使用_beginthreadex創建的線程就不該用CloseHandle釋放,因爲,當用_beginThread來創建,而用CloseHandle來關閉線程時,這時複製的全局結構就不會被釋放了,這就有了內存的泄漏。這就是很多資料所說的內存泄漏問題的真正的原因。

(2)不要在一個MFC程序中使用_beginthreadex()CreateThread()。這句話的意思是由於AfxBeginThread()是MFC封裝的啓動線程的函數,裏面包含了很多和MFC相關的啓動信息,而且封裝了一些常用的操作,使用起來也比較簡便。而用另外兩個函數就需要程序員對類型,安全性檢查進行更多的思考!

(3)用_beginthreadex()函數應該是最佳選擇,因爲_beginthreadex()函數是CRun-timeLibrary中的函數,函數的參數和數據類型都是CRun-timeLibrary中的類型,這樣在啓動線程時就不需要進行Windows數據類型和CRun-timeLibrary中的數據類型之間的轉化。減低了線程啓動時的資源消耗和時間的消耗!

(4)在C程序中,幾乎都要用到newdelete,難道只有使用_beginthreadex()?不,因爲MFC也是C類庫(只不過是Microsoft的C類庫,不是標準的C類庫),在MFC中也封裝了new和delete兩中運算符,所以用到new和delete的地方不一定非要使用_beginthreadex()函數,用其他兩個函數都可以!其實在程序中使用上面的哪個函數並不是絕對的,書的作者只不過是提了一個更佳的搭配方法,我在MFC程序中也經常使用_beginthreadex()CreateThread()這兩個函數,運行的效果也沒有多大的區別,有的時候只是需要你額外的進行一些類型檢查和其他的一些轉化操作,其餘沒有其他不妥! 創建線程只有一個方法是::CreateThread()_beginthreadex()AfxBeginThread()等內部都是調用這個函數的,因爲操作系統只提供這一個接口C靜態庫比WINDOWS出來還早,就別提多線程了,所以他對多線程的支持不是很好,但後悔也來不急,但也不能怪人家。

(5)C運行庫_beginthreadex()。他經過一些處理後,再調用CreateThread()如果要強制結束的話也最好用_endthreadex結束,因爲他也要一些處理。 總結上面的內容,當然《Windows核心編程》上面得說法是比較權威的。所以,在對線程的結構、運行還不是很瞭解的時候最好還是按照書上的來。這樣能夠避免一些可能出現的莫名奇妙的錯誤,也省去的一些其他結構處理的考慮。當你清楚地知道線程的結構與運行機制,以及瞭解各個函數對CreateThread函數的封裝的時候,大概那時候就能夠應用自如了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章