日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Win32多线程编程(2) — 线程控制

發布時間:2024/4/11 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Win32多线程编程(2) — 线程控制 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Win32線程控制只有是圍繞線程這一內核對象的創建、掛起、恢復、終結以及通信等操作,這些操作都依賴于Win32操作系統提供的一組API和具體編譯器的C運行時庫函數。本篇圍繞這些操作接口介紹在Windows系統下的多線程編程要點,后續將進一步涉及多線程通信的同步互斥等議題。

?

1.線程的創建(CreateThread)

每個線程必須擁有一個進入點函數,線程從這個進入點開始運行。主線程的進入點是main/WinMain函數,如果想在進程中創建一個輔助線程,則必須為該輔助線程指定一個進入點函數,這個函數稱為線程函數。

線程函數的定義如下:

typedef?DWORD?(WINAPI*?ThreadProc)(LPVOID?lpParam);?//?線程函數名稱ThreadProc可以是任意的

WINAPI是一個宏名,在?windef.h文件中有如下的聲明。

#define?WINAPI?__stdcall

__stdcall?是Windows標準?C/C++函數的調用方法。從底層上說,使用這種調用方法參數的進棧順序和標準?C調用(__cdecl?方法)是一樣的,都是從右到左,但是__stdcall?采用自動清棧的方式,而__cdecl?采用的是手工清棧方式。

Windows?規定,凡是由它來負責調用的函數都必須定義為__stdcall?類型。

ThreadProc是一個回調函數,即由Windows系統來負責調用的函數,所以此函數應定義為__stdcall類型。注意,如果沒有顯式說明的話,函數的調用方法是__cdecl。

Windows創建新線程的API是CreateThread,由該函數創建的線程將在調用者的虛擬地址空間內執行。函數原型如下:

// The CreateThread function creates a thread to execute within the virtual address space of the calling process.

// To create a thread that runs in the virtual address space of another process, use the CreateRemoteThread function.

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

??????????????????);

參數一為線程的安全屬性,一般設為NULL,表示使用默認安全屬性。

參數二為線程堆棧大小,一般設為NULL,表示使用默認堆棧大小,對應VC的/STACK:鏈接器選項。VC6默認的堆棧大小為1M,可通過“Project SettingsàLinkàStack allocations”設置堆棧大小;VC2005中,可在“項目屬性à配置屬性à鏈接器à系統”中設置堆棧大小。

參數三為線程函數的地址,傳遞函數指針或函數名ThreadProc。

參數四為傳遞給線程函數的參數,即ThreadProc的參數。其為LPVOID類型,對復雜的參數采用結構體或類按址傳遞。

參數五為線程創建參數,例如線程創建后是否立即啟動的開關選項。

參數六為內核給新創建的線程分配的線程ID號,為輸出參數。

用戶編寫多線程程序時,一般關注參數三和參數四足矣,其他可采用默認參數。

此函數執行成功后,將返回新建線程的線程句柄。lpStartAddress參數指定了線程函數的地址,新建線程將從此地址開始執行,直到?return?語句返回,線程運行結束,把控制權交給操作系統。

線程內核對象(kthread)

線程內核對象就是一個包含了線程狀態信息的數據結構。每一次對?CreateThread?函數的成功調用,系統都會在內部為新的線程分配一個內核對象。系統提供的管理線程的函數其實就是依靠訪問線程內核對象來實現管理的。在WinDbg中,可通過lkd> dt nt!_kthread查看線程內核對象數據結構,這里涉及到線程上下文(Context)、使用計數(Usage Count)和暫停計數(Suspend Count)等重要概念。

(1)線程上下文

線程的上下文本質上是一組處理器的寄存器,有正在執行程序中的指針及堆棧指針。上下文及其轉換的過程根據處理器的結構不同會有所不同,參考《線程的數據結構》。在WinDbg中,可以通過lkd> dt nt!_context命令來觀察上下文的數據結構。<WINNT.H>中定義了_CONTEXT結構。

大約每經?20ms,Windows?查看一次當前存在的所有線程內核對象。在這些對象中,只有一少部分是可調度的(沒有處于暫停狀態),Windows?選擇其中的一個內核對象,將它的CONTEXT(上下文)裝入?CPU的寄存器,這一過程稱為上下文切換。

用戶可調用GetThreadContext查看當前線程的用戶模式的上下文信息;調用SetThreadContext改變線程上下文,待下次調度進CPU時生效。其中,ContextFlags參數通過異或掩碼指定欲查看的寄存器。_KTHREAD::ContextSwitches為線程已切換的次數。

(2)使用計數

Usage Count成員記錄了線程內核對象的使用計數,這個計數說明了此內核對象被打開的次數。線程內核對象的存在與?Usage Count?的值息息相關,當這個值是0?的時候,系統就認為已經沒有任何進程在引用此內核對象了,于是線程內核對象就要從內存中撤銷。

只要線程沒有結束運行,Usage??Count?的值就至少為?1。在創建一個新的線程時,CreateThread?函數返回了線程內核對象的句柄,相當于打開一次新創建的內核對象,這也會促使?Usage Count?的值加1。所以創建一個新的線程后,初始狀態下?Usage Count?的值是2。之后,只要有進程打開此內核對象,就會使Usage Count的值加1。比如當有一個進程調用OpenThread函數打開這個線程內核對象后,Usage Count?的值會再次加?1。

// The OpenThread function opens an existing thread object.

HANDLE?OpenThread(

????????????????DWORD?dwDesiredAccess,??// access right

????????????????BOOL?bInheritHandle,????// handle inheritance option

????????????????DWORD?dwThreadId????????// thread identifier

????????????????);

由于對這個函數的調用會使?Usage Count?的值加1,所以在使用完它們返回的句柄后一定要調用?CloseHandle?函數進行關閉。關閉內核對象句柄的操作就會使?Usage Count?的值減?1。

還有一些函數僅僅返回內核對象的偽句柄,并不會創建新的句柄,當然也就不會影響Usage Count?的值。如果對這些偽句柄調用?CloseHandle?函數,那么?CloseHandle?就會忽略對自己的調用并返回?FALSE。對進程和線程來說,這些函數有:

// The GetCurrentProcess function retrieves a pseudo handle for the current process.

HANDLE?GetCurrentProcess(VOID);

// The GetCurrentThread function retrieves a pseudo handle for the current thread.

HANDLE?GetCurrentThread(VOID);

前面提到,新創建的線程在初始狀態下?Usage??Count?的值是?2。此時如果立即調用CloseHandle?函數來關閉CreateThread返回的句柄的話,Usage Count?的值將減為?1,但新創建的線程是不會被終止的。待線程函數返回,系統會使?Usage Count?的值由1減為0,線程的生命周期到此為止,系統將撤銷此線程內核對象,釋放其所占內存。

如果不關閉句柄的話,Usage Count?的值將永遠不會是?0,系統將永遠不會撤銷它占用的內存,這就會造成內存泄漏(當然,線程所在的進程結束后,該進程占用的所有資源都要釋放)。

(3)暫停計數

暫停計數參考下面的多線程狀態控制。

(4)主輔線程的執行同步

一般主線程應該后于輔助線程退出,如果主線程先退出,輔助線程尚在執行,將會出現意想不到的結果,因此主線程必須對輔助線程具有完全的控制權。

一個可執行對象有兩種狀態,未受信(nonsignaled)和受信(signaled)狀態。線程內核對象只有當線程過程運行結束時才達到受信狀態??烧{用WaitForSingleObject/WaitForMultipleObjects函數在線程內核對象(HANDLE)上等待,以便主輔線程同步。

?

2.線程的狀態控制(SuspendThread/ResumeThread、Sleep)

線程在創建后和終止前之間的狀態,用戶可感知或控制的狀態主要有運行和暫停(中斷)兩種,對應的操作為掛起(Suspend)和恢復(Resume)。

線程內核對象中的Suspend Count(_KTHREAD::SuspendCount)用于指明線程的暫停計數。

當調用CreateProcess(創建進程的主線程)或CreateThread函數時,線程的內核對象被創建了,它的暫停計數被初始化為1(即出于暫停狀態),這可以阻止新創建的線程被立即調度進CPU中。因為線程初始化需要時間,當線程完全初始化好了之后,CreateProcessCreateThread檢查dwCreationFlags參數是否傳遞了CREATE_SUSPEND標志,如果傳遞了,這些函數就返回,同時新線程處于暫停狀態。如果尚未傳遞該標志,那么線程的暫停計數將被遞減為0。當線程的暫停計數是?0的時候,該線程就進入可調度狀態。

創建線程的時候,若指定CREATE_SUSPEND標志,則用戶有機會在線程執行任何代碼之前改變線程的運行環境(如后面討論的優先級)。然后,需要調用ResumeThread函數,減少線程的暫停計數至0,使線程恢復運行,進入可調度狀態。

// The ResumeThread function?decrements?a thread's suspend count. When the suspend count is decremented to zero, the execution of the thread is resumed.

DWORD?ResumeThread(

?????????????????HANDLE?hThread???// handle to thread

?????????????????);

后續可調用SuspendThread函數來暫停一個線程的運行,因為該API傳遞的是句柄,故該函數可跨進程調用,即在一個線程中暫停另一個線程。與ResumeThread相反,SuspendThread函數的調用會增加線程的暫停計數。

// The SuspendThread function suspends the specified thread.

DWORD?SuspendThread(

??????????????????HANDLE?hThread???// handle to thread

??????????????????);

只有當線程的暫停計數是?0的時候,線程才能進入可調度狀態。因此,必須注意SuspendThread/ResumeThread函數調用次數的匹配,以便正確控制。

SuspendThread/ResumeThread函數對于線程狀態的控制具有很強的針對性,另外一種簡單的替代方案是調用Sleep函數,讓調用線程睡一會兒,給其他的線程一個調度機會,從而提供一種線程切換緩沖機制。

// The Sleep function suspends the execution of the current thread for the specified interval.

// To enter an alertable wait state, use the SleepEx function.

VOID?Sleep(

??????????DWORD?dwMilliseconds???// sleep time

??????????);

Sleep函數在線程的while(1)?死循環中非常實用,因為一個線程如果while(1)輪回,則CPU使用率一般會飆升,置系統與卡死狀態。特殊地,Sleep(0)表示調用線程主動放棄時間片的剩余部分,它強制系統調度其他線程。但是,系統有可能重新調度剛剛調用了Sleep的那個線程。?由于該調用本身占用時鐘周期,也會讓當前線程放棄時間片,交出控制權,從而使別的線程有機會執行。當然,可直接調用SwitchToThread()執行線程切換。

?

3.線程的優先級控制(GetThreadPriority/SetThreadPriority)

線程的狀態轉換、優先級及調度方案,請參考《線程的調度》。

對于一個線程,我們可以調用GetThreadPriority函數來獲取其優先級。

// The GetThreadPriority function retrieves the priority value for the specified thread. This value, together with the priority class of the thread's process, determines the thread's base-priority level.

int?GetThreadPriority(

????????????????????HANDLE?hThread???// handle to thread

????????????????????);

系統可以動態地調整線程的優先級,當系統希望這個線程處理窗口消息、I/O調用或是系統發現3-4秒內這個線程一直迫切地需要一個時間片時,它會將這個線程的優先級調整為15并可以運行雙倍的時間片。對線程優先級的動態調整是通過調用SetThreadPriority函數實現的。

// The SetThreadPriority function sets the priority value for the specified thread. This value, together with the priority class of the thread's process, determines the thread's base priority level.

BOOL?SetThreadPriority(

?????????????????????HANDLE?hThread,?// handle to the thread

?????????????????????int?nPriority???// thread priority level

?????????????????????);

????對于一般的應用程序開發,按照常規的優先級配置即可,很少涉及優先級的操作。

?

4.線程的停止(ExitThread/TerminateThread)

線程過程的結束主要有兩種情形,即自然終止和人為中止。

自然終止,主要是指線程函數自然返回的情況,是壽終正寢的圓寂。人為中止是通過暴力手段將尚未完成使命的線程謀殺,是死于非命的夭折。

線程自然終止時,會發生下列事件:

l??在線程函數中創建的所有?C++對象將通過它們各自的析構函數被正確地銷毀。

l??該線程使用的堆棧將被釋放。

l??系統將線程內核對象中?Exit Code(退出代碼)的值由?STILL_ACTIVE?設置為線程函數的返回值。

l??系統將遞減線程內核對象中?Usage Code(使用計數)的值。

線程結束后的退出代碼可以被其他線程用GetExitCodeThread函數檢測到,所以可以當做自定義的返回值來表示線程的執行結果。

人為中止主要有兩種手段:

(1)調用ExitThread(CRT中的exit)結束當前線程(調用線程);

// The ExitThread function ends a thread.

VOID?ExitThread(

??????????????DWORD?dwExitCode???// exit code for this thread

??????????????);

ExitThread?函數會中止當前線程的運行,促使系統釋放掉所有此線程使用的資源。但是,線程函數中申請的C++資源,典型的如C++類(class)卻不能得到正確地清除。這是因為C++對象的析構函數是通過atexit掛接到線程中,在exit退出時(main/WinMain或線程過程返回之后)調用。若線程過程被終止,則CRT越過atexit,從而使C++資源不能正確釋放。所以,結束線程最好的方法還是讓線程自然返回。

(2)調用TerminateThread進行跨線程中止。

// The TerminateThread function terminates a thread.

BOOL?TerminateThread(

???????????????????HANDLE?hThread,????// handle to thread

???????????????????DWORD?dwExitCode???// exit code

???????????????????);

因為該API傳遞的是句柄,故該函數可跨線程調用,即在一個線程中中止另一個線程。

這是一個被強烈建議避免使用的函數,因為一旦執行這個函數,程序無法預測目標線程會在何處被中止,其結果就是目標線程可能根本沒有機會來做清除工作,如線程中打開的文件和申請的內存都不會被釋放。另外,使用TerminateThread?函數中止線程的時候,系統不會釋放線程使用的堆棧。所以建議讀者在編程的時候盡量讓線程自己退出,如果主線程要求某個線程結束,可以通過各種方法通知線程,線程收到通知后自行退出。只有在迫不得已的情況下,才使用?TerminateThread?函數終止線程。

無論是自然終止還是人為中止,它們都將使使用計數減1。此時,我們通過調用GetExitCodeThread函數來獲取線程函數的返回值。最后,必須調用CloseHandle關閉線程句柄,使使用計數再減1。至此,該線程內核對象結束生命的旅程。

總之,始終應該讓線程正常退出,即使它的線程函數自然返回。通知線程退出的方法很多,如設置全局變量、使用事件對象等,這涉及到下一節線程間的通信問題。

?

5.用CRT的_beginthreadex代替操作系統的CreateThread

(1)為IDE選擇正確的RTL(Run Time Library)

在VC6中,“Project Settings?à?C/C++?à?Category(Code Generation)à?Use run-time library”共有六個選項供選擇:

/ML:Single-Threaded*

/MLd:Debug Single-Threaded

/MT:Multithreaded

/MTd:Debug Multithreaded

/MD:Multithreaded DLL

/MDd:Debug Multithreaded DLL

其中/ML[d]和/MT[d]主要針對常規多線程應用程序的RTL版本選擇,/MD[d]主要針對多線程LIB或DLL工程中的RTL版本選擇。

VC6常規工程的默認選項為/ML[d],MFC工程的默認選項為/MD[d]。對于需要多線程支持的工程中,如果不設定/MT或/MD選項,則可能出現“error LNK2001: unresolved external symbol __beginthreadex”或“error C2065: '_beginthreadex' : undeclared identifier”。

在VC2005中,“項目屬性?à?配置屬性?à?C/C++??à?代碼生成”中提供了四種選擇,

多線程(/MT

多線程調試(/MTd

多線程DLL(/MD

多線程調試DLL(/MDd

VC2005工程默認選項為/MD[d]。

不同的鏈接選項,鏈接器將選擇不同的鏈接庫進行鏈接。參考《C Run-Time Libraries (CRT)》和《/MD, /ML, /MT, /LD???(Use Run-Time Library)》。

(2)用C運行時庫的_beginthreadex代替操作系統的CreateThread來創建線程

在實際的開發過程中,一般不直接使用Windows系統提供的CreateThread函數創建線程,而是使用?C/C++運行期函數_beginthread(ex)/_endthread(ex)

使用_beginthread無法創建帶有安全屬性的新線程,無法創建暫停的線程,也無法獲得線程ID。_endthread的情況類似,它不帶參數,這意味這線程的退出代碼必須硬編碼為0。一般不調用_beginthread/_endthread,而是調用其擴展版本_beginthreadex/_endthreadex。

事實上,C/C++運行期庫提供CreateThread加強版的_beginthreadex,是為了多線程同步的需要。在早期的單線程C運行庫中有許多的全局變量,如errno、strerror等,它們可以用來表示線程當前的一些狀態。

但是在多線程程序設計中,每個線程必須有惟一的狀態,否則這些變量記錄的信息就不會準確了。比如,全局變量errno?用于表示調用運行期函數失敗后的錯誤代碼。如果所有線程共享一個errno?的話,在一個線程產生的錯誤代碼就會影響到另一個線程。為了解決這個問題,每個線程都需要有自己的errno?變量。

要想使運行期為每個線程都設置狀態變量,必須在創建線程的時候調用運行期提供的_beginthreadex,讓運行期設置了相關變量后再去調用Windows系統提供的?CreateThread函數。

_beginthreadex的參數與CreateThread函數對應,函數的參數和數據類型都是C Run-time Library中的類型,在使用時候需要強制類型轉換。

// /Microsoft Visual Studio/VC98/CRT/SRC/THREADEX.C

unsigned long?__cdecl?_beginthreadex(

????????void *security,

????????unsigned?stacksize,

????????unsigned (__stdcall*?initialcode)(void*),

????????void *?argument,

????????unsigned?createflag,

????????unsigned *thrdaddr)

{

_ptiddata?ptd;??????????????????/* pointer to per-thread data */

// ……

ptd->_initaddr?= (void *)initialcode;

ptd->_initarg?=?argument;

ptd->_thandle?= (unsigned long)(-1L);

// ……

CreateThread(security,

???????stacksize,

???????_threadstartex,

???????(LPVOID)ptd,??// pointer to per-thread data(_ptiddata?ptd)

???????createflag,

???????thrdaddr));

????// ……

}

線程啟動函數為_threadstartex,其參數為線程局部存儲(TLS)結構_ptiddata?ptd,其中包含了線程函數ptd->_initaddr和線程參數ptd->_initarg。關于線程局部存儲(TLS),參考后續議題。實際上_ptiddata結構中定義了_terrno、_token、_errmsg等單線程全局變量。

struct?_tiddata

{

????unsigned long???_tid;???????????/* thread ID */

????unsigned long???_thandle;???????/* thread handle */

???

????int?????_terrno;????????????????/* errno value */

unsigned long???_holdrand;??????/* rand() seed value */

char?*??????_token;?????????????/* ptr to strtok() token */

?

char?*??????_errmsg;????????????/* ptr to strerror()/_strerror() buff */

?

????void?*??????_initaddr;??????????/* initial user thread address */

void?*??????_initarg;???????????/* initial user thread argument */

}

typedef struct?_tiddata?*?_ptiddata;

// _threadstartex() - New thread begins here

static unsigned long?WINAPI?_threadstartex(void *ptd)

{

// ……

// Call fp initialization, if necessary

if (?_FPmtinit?!=?NULL?)

(*_FPmtinit)();

// ……

_endthreadex(((unsigned (WINAPI?*)(void*))(((_ptiddata)ptd)->_initaddr))(((_ptiddata)ptd)->_initarg));

}

線程啟動函數_threadstartex中,進行相關的初始化(_FPmtinit)后,開始調用ptd->_initaddr(ptd->_initarg)執行真正的線程過程。傳遞線程過程返回碼調用_endthreadex函數。

// _endthreadex() - Terminate the calling thread

void?__cdecl?_endthreadex(unsigned?retcode)

_endthreadex函數釋放線程局部存儲(TLS)數據_ptiddata?ptd,調用ExitThread結束當前線程的運行。故這里有了不調用ExitThread的另一個理由:即它會阻止線程的_ptiddata內存的釋放。故建議使用_endthreadex替代ExitThread調用,當然,這也是不應該提倡的做法。

參考《_beginthreadex和CreateThread》。

總結

以上是生活随笔為你收集整理的Win32多线程编程(2) — 线程控制的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。