Windows核心编程 第六章 线程基础知识 (上)
第6章?線程的基礎(chǔ)知識(shí)
? ? 理解線程是非常關(guān)鍵的,因?yàn)槊總€(gè)進(jìn)程至少需要一個(gè)線程。本章將更加詳細(xì)地介紹線程的知識(shí)。尤其是要講述進(jìn)程與線程之間存在多大的差別,它們各自具有什么作用。還要介紹系統(tǒng)如何使用線程內(nèi)核對(duì)象來(lái)管理線程。與進(jìn)程內(nèi)核對(duì)象一樣,線程內(nèi)核對(duì)象也擁有屬性,我們將要觀察許多用于查詢(xún)和修改這些屬性的函數(shù)。此外還要介紹可以在進(jìn)程中創(chuàng)建和生成更多的線程時(shí)所用的函數(shù)。
? ? 第4章介紹了進(jìn)程是由兩個(gè)部分構(gòu)成的,一個(gè)是進(jìn)程內(nèi)核對(duì)象,另一個(gè)是地址空間。同樣,線程也是由兩個(gè)部分組成的:
????? 一個(gè)是線程的內(nèi)核對(duì)象,操作系統(tǒng)用它來(lái)對(duì)線程實(shí)施管理。內(nèi)核對(duì)象也是系統(tǒng)用來(lái)存放線程統(tǒng)計(jì)信息的地方。
? 另一個(gè)是線程堆棧,它用于維護(hù)線程在執(zhí)行代碼時(shí)需要的所有函數(shù)參數(shù)和局部變量(第1 6章將進(jìn)一步介紹系統(tǒng)如何管理線程堆棧) 。
? ? 第4章中講過(guò),進(jìn)程是不活潑的。進(jìn)程從來(lái)不執(zhí)行任何東西,它只是線程的容器。線程總是在某個(gè)進(jìn)程環(huán)境中創(chuàng)建的,而且它的整個(gè)壽命期都在該進(jìn)程中。這意味著線程在它的進(jìn)程地址空間中執(zhí)行代碼,并且在進(jìn)程的地址空間中對(duì)數(shù)據(jù)進(jìn)行操作。因此,如果在單進(jìn)程環(huán)境中,你有兩個(gè)或多個(gè)線程正在運(yùn)行,那么這兩個(gè)線程將共享單個(gè)地址空間。這些線程能夠執(zhí)行相同的代碼,對(duì)相同的數(shù)據(jù)進(jìn)行操作。這些線程還能共享內(nèi)核對(duì)象句柄,因?yàn)榫浔硪蕾?lài)于每個(gè)進(jìn)程而不是每個(gè)線程存在。
???如你所見(jiàn),進(jìn)程使用的系統(tǒng)資源比線程多得多,原因是它需要更多的地址空間。為進(jìn)程創(chuàng)建一個(gè)虛擬地址空間需要許多系統(tǒng)資源。系統(tǒng)中要保留大量的記錄,這要占用大量的內(nèi)存。另外,由于. e x e和. d l l文件要加載到一個(gè)地址空間,因此也需要文件資源。而線程使用的系統(tǒng)資源要少得多。實(shí)際上,線程只有一個(gè)內(nèi)核對(duì)象和一個(gè)堆棧,保留的記錄很少,因此需要很少的內(nèi)存。
? ? 由于線程需要的開(kāi)銷(xiāo)比進(jìn)程少,因此始終都應(yīng)該設(shè)法用增加線程來(lái)解決編程問(wèn)題,而要避免創(chuàng)建新的進(jìn)程。但是,這個(gè)建議并不是一成不變的。許多程序設(shè)計(jì)用多個(gè)進(jìn)程來(lái)實(shí)現(xiàn)會(huì)更好些。應(yīng)該懂得權(quán)衡利弊,經(jīng)驗(yàn)會(huì)指導(dǎo)你的編程實(shí)踐。
? ? 在詳細(xì)介紹線程之前,首先花一點(diǎn)時(shí)間講一講如何正確地在應(yīng)用程序結(jié)構(gòu)中使用線程。
6.1 何時(shí)創(chuàng)建線程
????線程用于描述進(jìn)程中的運(yùn)行路徑。每當(dāng)進(jìn)程被初始化時(shí),系統(tǒng)就要?jiǎng)?chuàng)建一個(gè)主線程。該線程與C / C + +運(yùn)行期庫(kù)的啟動(dòng)代碼一道開(kāi)始運(yùn)行,啟動(dòng)代碼則調(diào)用進(jìn)入點(diǎn)函數(shù)( m a i n、w m a i n、Wi n M a i n或w Wi n M a i n) ,并且繼續(xù)運(yùn)行直到進(jìn)入點(diǎn)函數(shù)返回并且 C / C + +運(yùn)行期庫(kù)的啟動(dòng)代碼調(diào)用E x i t P r o c e s s為止。對(duì)于許多應(yīng)用程序來(lái)說(shuō),這個(gè)主線程是應(yīng)用程序需要的唯一線程。不過(guò),
進(jìn)程能夠創(chuàng)建更多的線程來(lái)幫助執(zhí)行它們的操作。
每個(gè)計(jì)算機(jī)都擁有一個(gè)功能非常強(qiáng)大的資源,即 C P U。讓C P U閑置起來(lái)是絕對(duì)沒(méi)有道理的(如果忽略節(jié)省電能問(wèn)題的話) 。為了使C P U處于繁忙狀態(tài)之中,可以讓它執(zhí)行各種不同的工作。
下面是一些例子:
????? 可以打開(kāi)Microsoft Windows 2000配備的內(nèi)容索引服務(wù)程序。它能夠創(chuàng)建一個(gè)低優(yōu)先級(jí)的線程,以便定期打開(kāi)你的磁盤(pán)驅(qū)動(dòng)器上的文件內(nèi)容并給內(nèi)容做索引。若要找到一個(gè)文件,可以打開(kāi)Search Result(搜索結(jié)果)窗口(方法是單擊 S t a r t按鈕,從S e a r c h菜單中選定For Files Or Folders) ,再將你的搜索條件輸入Containing Te x t域。這時(shí)就可以搜索到索引,相關(guān)的文件就會(huì)立即顯示出來(lái)。內(nèi)容索引服務(wù)程序大大改進(jìn)了性能,因?yàn)槊看嗡阉鞑槐卮蜷_(kāi)、掃描和關(guān)閉磁盤(pán)驅(qū)動(dòng)器上的每個(gè)文件。
????? 可以使用Windows 2000配備的磁盤(pán)碎片整理軟件。通常情況下,這種類(lèi)型的實(shí)用程序擁有許多管理選項(xiàng),一般用戶(hù)可能不懂,比如該實(shí)用程序應(yīng)該相隔多長(zhǎng)時(shí)間運(yùn)行一次,何時(shí)運(yùn)行。使用低優(yōu)先級(jí)線程,可以在后臺(tái)運(yùn)行該實(shí)用程序,并且在系統(tǒng)空閑時(shí)對(duì)驅(qū)動(dòng)器進(jìn)行碎片整理。
????? 可以很容易地設(shè)想將來(lái)版本的編譯器,每當(dāng)暫停鍵入時(shí),它就可以自動(dòng)編譯你的源代碼文件。輸出窗口可以向你(幾乎)實(shí)時(shí)顯示警告和出錯(cuò)信息。當(dāng)鍵入變量和函數(shù)名時(shí)出現(xiàn)錯(cuò)誤時(shí),就能立即發(fā)現(xiàn)。在某種程度上講, Microsoft Visual Studio已經(jīng)實(shí)現(xiàn)了這個(gè)功能,使用Wo r k s p a c e的C l a s s Vi e w窗格,就能夠看到這些信息。
????? 電子表格應(yīng)用程序能夠在后臺(tái)執(zhí)行各種計(jì)算。
????? 字處理程序能夠執(zhí)行重新分頁(yè)、拼寫(xiě)和語(yǔ)法檢查及在后臺(tái)進(jìn)行打印。
????? 文件可以在后臺(tái)拷貝到其他介質(zhì)中。
? We b瀏覽器在后臺(tái)與它們的服務(wù)器進(jìn)行通信。因此,在來(lái)自當(dāng)前 We b站點(diǎn)的結(jié)果輸入之前,用戶(hù)可以縮放瀏覽器的窗口或者轉(zhuǎn)到另一個(gè)We b站點(diǎn)。
這些例子中,有一個(gè)重要問(wèn)題應(yīng)該注意,那就是多線程能夠簡(jiǎn)化應(yīng)用程序的用戶(hù)界面。如果每當(dāng)停止鍵入時(shí),編譯器建立了你的應(yīng)用程序,那么就沒(méi)有必要提供 B u i l d菜單選項(xiàng)。文字處理應(yīng)用程序不需要Check Spelling(拼寫(xiě)檢查)和Check Grammar(語(yǔ)法檢查)菜單選項(xiàng)。
在We b瀏覽器的例子中,注意,將不同的線程用于 I / O(網(wǎng)絡(luò)、文件或其他) ,應(yīng)用程序的用戶(hù)界面就能夠始終保持工作狀態(tài)。比如有一個(gè)應(yīng)用程序負(fù)責(zé)給數(shù)據(jù)庫(kù)記錄進(jìn)行排序、打印文檔或拷貝文件。如果將獨(dú)立的線程用于處理這個(gè)與 I / O相關(guān)的任務(wù),用戶(hù)就可以在進(jìn)程中繼續(xù)使用應(yīng)用程序界面來(lái)取消操作。
設(shè)計(jì)一個(gè)擁有多線程的應(yīng)用程序,就會(huì)擴(kuò)大該應(yīng)用程序的功能。我們?cè)谙乱徽轮锌梢钥吹?#xff0c;每個(gè)線程被分配了一個(gè)C P U。因此,如果你的計(jì)算機(jī)擁有兩個(gè)C P U,你的應(yīng)用程序中有兩個(gè)線程,那么兩個(gè)C P U都將處于繁忙狀態(tài)。實(shí)際上,你是讓兩個(gè)任務(wù)在執(zhí)行一個(gè)任務(wù)的時(shí)間內(nèi)完成操作。
每個(gè)進(jìn)程至少擁有一個(gè)線程。因此,如果你在應(yīng)用程序中不執(zhí)行任何特殊的操作,在多進(jìn)程操作系統(tǒng)上運(yùn)行,就能夠得到許多好處。例如,可以建立一個(gè)應(yīng)用程序,并同時(shí)使用文字處理程序(我常常這樣做) 。如果計(jì)算機(jī)擁有兩個(gè)C P U,那么該應(yīng)用程序就可以在一個(gè)處理器上執(zhí)行,而另一個(gè)處理器則負(fù)責(zé)處理文檔。另外,如果編譯器出現(xiàn)一個(gè)錯(cuò)誤,導(dǎo)致它的線程進(jìn)入一個(gè)無(wú)限循環(huán),仍然可以使用其他的進(jìn)程(1 6位Wi n d o w s和M S - D O S應(yīng)用程序則不行) 。
?
6.2 何時(shí)不能創(chuàng)建線程
至今為止,一直在討論多線程應(yīng)用程序的優(yōu)點(diǎn)。雖然多線程應(yīng)用程序的優(yōu)點(diǎn)很多,但是它也存在某些不足之處。有些開(kāi)發(fā)人員認(rèn)為,解決問(wèn)題的方法是將它分割成多個(gè)線程。這種想法是完全錯(cuò)誤的。
線程確實(shí)是非常有用的,但是,當(dāng)使用線程時(shí),在解決原有的問(wèn)題時(shí)可能產(chǎn)生新的問(wèn)題。
????例如,你開(kāi)發(fā)了一個(gè)文字處理應(yīng)用程序,并且想要讓打印函數(shù)作為它自己的線程來(lái)運(yùn)行。這聽(tīng)起來(lái)是個(gè)很好的主意,因?yàn)橛脩?hù)可以在打印文檔時(shí)立即回頭著手編輯文檔。但是,這意味著文檔中的數(shù)據(jù)可能在文檔打印時(shí)變更。也許最好是不要讓打印操作在它自己的線程中發(fā)生,不過(guò)這種“方案”看起來(lái)有點(diǎn)兒極端。如果你讓用戶(hù)編輯另一個(gè)文檔,但是鎖定正在打印的文檔,使得打印結(jié)束前該文檔不能修改,那將會(huì)怎樣呢?這里還有第三種思路,將文檔拷貝到一個(gè)臨
時(shí)文件,然后打印該臨時(shí)文件的內(nèi)容,并讓用戶(hù)修改原始文檔。當(dāng)包含該文檔的臨時(shí)文件結(jié)束打印時(shí),刪除臨時(shí)文件。
如你所見(jiàn),線程能夠解決某些問(wèn)題,但是卻又會(huì)產(chǎn)生新的問(wèn)題。在開(kāi)發(fā)應(yīng)用程序的用戶(hù)界面時(shí),很可能出現(xiàn)對(duì)線程的另一種誤用。幾乎在所有的應(yīng)用程序中,所有用戶(hù)界面的組件(窗口)應(yīng)該共享同一個(gè)線程。單個(gè)線程應(yīng)該創(chuàng)建窗口的所有子窗口。有時(shí)在不同的線程上創(chuàng)建不同的窗口是有用的,不過(guò)這種情況確實(shí)非常少見(jiàn)。
通常情況下,一個(gè)應(yīng)用程序擁有一個(gè)用戶(hù)界面線程,用于創(chuàng)建所有窗口,并且有一個(gè)G e t M e s s a g e循環(huán)。進(jìn)程中的所有其他線程都是工作線程,它們與計(jì)算機(jī)或 I / O相關(guān)聯(lián),但是這些線程從不創(chuàng)建窗口。另外,一個(gè)用戶(hù)界面線程通常擁有比工作線程更高的優(yōu)先級(jí),因此用戶(hù)界面負(fù)責(zé)向用戶(hù)作出響應(yīng)。
雖然單個(gè)進(jìn)程擁有多個(gè)用戶(hù)界面線程的情況并不多見(jiàn), 但是這種情況有著某種有效的用途。Windows Explorer為每個(gè)文件夾窗口創(chuàng)建了一個(gè)獨(dú)立的線程。它使你能夠?qū)⑽募囊粋€(gè)文件夾拷貝到另一個(gè)文件夾,并且仍然可以查看你的系統(tǒng)上的其他文件夾。另外,如果 E x p l o r e r中存在一個(gè)錯(cuò)誤,那么負(fù)責(zé)處理文件夾的線程可能崩潰,但是仍然能夠?qū)ζ渌募A進(jìn)行操作,至少在執(zhí)行的操作導(dǎo)致其他文件夾也崩潰之前,仍然可以對(duì)它們進(jìn)行操作(關(guān)于線程和用戶(hù)界面
的詳細(xì)說(shuō)明,參見(jiàn)第2 6和2 7章) 。
上述內(nèi)容的實(shí)質(zhì)是應(yīng)該慎重地使用多線程。不要想用就用。僅僅使用賦予進(jìn)程的主線程,就能夠編寫(xiě)出許多非常有用的和功能強(qiáng)大的應(yīng)用程序。
6.3 編寫(xiě)第一個(gè)線程函數(shù)
? ?每個(gè)線程必須擁有一個(gè)進(jìn)入點(diǎn)函數(shù),線程從這個(gè)進(jìn)入點(diǎn)開(kāi)始運(yùn)行。前面已經(jīng)介紹了主線程的進(jìn)入點(diǎn)函數(shù):即m a i n、w m a i n、Wi n M a i n或w Wi n M a i n。如果想要在你的進(jìn)程中創(chuàng)建一個(gè)輔助線程,它必定也是個(gè)進(jìn)入點(diǎn)函數(shù),類(lèi)似下面的樣子:
?
? ?你的線程函數(shù)可以執(zhí)行你想要它做的任何任務(wù)。最終,線程函數(shù)到達(dá)它的結(jié)尾處并且返回。這時(shí),線程終止運(yùn)行,該堆棧的內(nèi)存被釋放,同時(shí),線程的內(nèi)核對(duì)象的使用計(jì)數(shù)被遞減。如果使用計(jì)數(shù)降為0,線程的內(nèi)核對(duì)象就被撤消。與進(jìn)程內(nèi)核對(duì)象的情況相同,線程內(nèi)核對(duì)象的壽命至少可以達(dá)到它們相關(guān)聯(lián)的線程那樣長(zhǎng),不過(guò),該對(duì)象的壽命可以遠(yuǎn)遠(yuǎn)超過(guò)線程本身的壽命。
下面對(duì)線程函數(shù)的幾個(gè)問(wèn)題作一說(shuō)明:
????? 主線程的進(jìn)入點(diǎn)函數(shù)的名字必須是m a i n、w m a i n、Wi n M a i n或w Wi n M a i n,與這些函數(shù)不同的是,線程函數(shù)可以使用任何名字。實(shí)際上,如果在應(yīng)用程序中擁有多個(gè)線程函數(shù),必須為它們賦予不同的名字,否則編譯器/鏈接程序會(huì)認(rèn)為你為單個(gè)函數(shù)創(chuàng)建了多個(gè)實(shí)現(xiàn)函數(shù)。
? 由于給你的主線程的進(jìn)入點(diǎn)函數(shù)傳遞了字符串參數(shù),因此可以使用 A N S I / U n i c o d e版本的進(jìn)入點(diǎn)函數(shù):m a i n / w m a i n和Wi n M a i n / w Wi n M a i n。可以給線程函數(shù)傳遞單個(gè)參數(shù),參數(shù)的含義由你而不是由操作系統(tǒng)來(lái)定義。因此,不必?fù)?dān)心 A N S I / U n i c o d e問(wèn)題。
? 線程函數(shù)必須返回一個(gè)值,它將成為該線程的退出代碼。這與 C / C + +運(yùn)行期庫(kù)關(guān)于讓主線程的退出代碼作為進(jìn)程的退出代碼的原則是相似的。
? 線程函數(shù)(實(shí)際上是你的所有函數(shù))應(yīng)該盡可能使用函數(shù)參數(shù)和局部變量。當(dāng)使用靜態(tài)變量和全局變量時(shí),多個(gè)線程可以同時(shí)訪問(wèn)這些變量,這可能破壞變量的內(nèi)容。然而,參數(shù)和局部變量是在線程堆棧中創(chuàng)建的,因此它們不太可能被另一個(gè)線程破壞。
6.4 CreateThread函數(shù)
? ? 前面已經(jīng)講述了調(diào)用C r e a t e P r o c e s s函數(shù)時(shí)如何創(chuàng)建進(jìn)程的主線程。如果想要?jiǎng)?chuàng)建一個(gè)或多個(gè)輔助函數(shù),只需要讓一個(gè)已經(jīng)在運(yùn)行的線程來(lái)調(diào)用C r e a t e T h r e a d:
?
? ? 當(dāng)C r e a t e T h r e a d被調(diào)用時(shí),系統(tǒng)創(chuàng)建一個(gè)線程內(nèi)核對(duì)象。該線程內(nèi)核對(duì)象不是線程本身,而是操作系統(tǒng)用來(lái)管理線程的較小的數(shù)據(jù)結(jié)構(gòu)。可以將線程內(nèi)核對(duì)象視為由關(guān)于線程的統(tǒng)計(jì)信息組成的一個(gè)小型數(shù)據(jù)結(jié)構(gòu)。這與進(jìn)程和進(jìn)程內(nèi)核對(duì)象之間的關(guān)系是相同的。
? ? 系統(tǒng)從進(jìn)程的地址空間中分配內(nèi)存,供線程的堆棧使用。新線程運(yùn)行的進(jìn)程環(huán)境與創(chuàng)建線程的環(huán)境相同。因此,新線程可以訪問(wèn)進(jìn)程的內(nèi)核對(duì)象的所有句柄、進(jìn)程中的所有內(nèi)存和在這個(gè)相同的進(jìn)程中的所有其他線程的堆棧。這使得單個(gè)進(jìn)程中的多個(gè)線程確實(shí)能夠非常容易地互相通信。
? ? 注意 C r e a t e T h r e a d函數(shù)是用來(lái)創(chuàng)建線程的 Wi n d o w s函數(shù)。不過(guò),如果你正在編寫(xiě)C / C + +代碼,決不應(yīng)該調(diào)用 C r e a t e T h r e a d。相反,應(yīng)該使用Visual C++運(yùn)行期庫(kù)函數(shù)_ b e g i n t h r e a d e x。如果不使用M i c r o s o f t的Visual C++編譯器,你的編譯器供應(yīng)商有它自己的C r e a t e T h r e d替代函數(shù)。不管這個(gè)替代函數(shù)是什么,你都必須使用。本章后面將要介紹_ b e g i n t h r e a d e x能夠做什么,它的重要性何在。
這就是Create Thread函數(shù)的概述,下面各節(jié)將要具體介紹C r e a t e T h r e a d的每個(gè)參數(shù)。
6.4.1 psa
p s a參數(shù)是指向S E C U R I T Y _ AT T R I B U T E S結(jié)構(gòu)的指針。如果想要該線程內(nèi)核對(duì)象的默認(rèn)安全屬性,可以(并且通常能夠)傳遞 N U L L。如果希望所有的子進(jìn)程能夠繼承該線程對(duì)象的句柄,必須設(shè)定一個(gè)S E C U R I T Y _ AT T R I B U T E S結(jié)構(gòu),它的b I n h e r i t H a n d l e成員被初始化為T R U E。詳細(xì)信息參見(jiàn)第3章。
6.4.2 cbStack
? ? c b S t a c k參數(shù)用于設(shè)定線程可以將多少地址空間用于它自己的堆棧。每個(gè)線程擁有它自己的堆棧。當(dāng)C r e a t e P r o c e s s啟動(dòng)一個(gè)進(jìn)程時(shí),它就在內(nèi)部調(diào)用 C r e a t e T h r e a d來(lái)對(duì)進(jìn)程的主線程進(jìn)行初始化。對(duì)于c b S t a c k參數(shù)來(lái)說(shuō),C r e a t e P r o c e s s使用存放在可執(zhí)行文件中的一個(gè)值。可以使用鏈接程序的/ S TA C K開(kāi)關(guān)來(lái)控制這個(gè)值:
/STACK:[reserve][.commit]
r e s e r v e參數(shù)用于設(shè)定系統(tǒng)應(yīng)該為線程堆棧保留的地址空間量。默認(rèn)值是 1 MB。C o m m i t參數(shù)用于設(shè)定開(kāi)始時(shí)應(yīng)該承諾用于堆棧保留區(qū)的物理存儲(chǔ)器的容量。默認(rèn)值是 1頁(yè)。當(dāng)線程中的代碼執(zhí)行時(shí),可能需要多個(gè)頁(yè)面的存儲(chǔ)器。當(dāng)線程溢出它的堆棧時(shí),就生成一個(gè)異常條件(關(guān)于線程堆棧和堆棧溢出的異常條件的詳細(xì)說(shuō)明,參見(jiàn)第 1 6章,關(guān)于一般異常條件的處理的詳細(xì)說(shuō)明,參見(jiàn)第2 3章) 。系統(tǒng)抓取該異常條件,并且將另一頁(yè)(或者你為 c o m m i t參數(shù)設(shè)定的任何值)用于保留空間,這使得線程的堆棧能夠根據(jù)需要?jiǎng)討B(tài)地?cái)U(kuò)大。
當(dāng)調(diào)用C r e a t e T h r e a d時(shí),如果傳遞的值不是0,就能使該函數(shù)將所有的存儲(chǔ)器保留并分配給線程的堆棧。由于所有的存儲(chǔ)器預(yù)先作了分配,因此可以確保線程擁有指定容量的可用堆棧存儲(chǔ)器。保留空間的容量既可以是/ S TA C K鏈接程序設(shè)定的容量,也可以是C b S t a c k的值,誰(shuí)大就用誰(shuí)。分配的存儲(chǔ)器容量應(yīng)該與傳遞的 c b S t a c k值相一致。如果將 0傳遞給 C b S t a c k參數(shù),C r e a t e T h r e a d就保留一個(gè)區(qū)域,并且將鏈接程序嵌入 . e x e文件的/ S TA C K鏈接程序開(kāi)關(guān)信息指明的存儲(chǔ)器容量分配給線程堆棧。
6.4.3 pfnStartAddr和p v P a r a m
? ? p f n S t a r t A d d r參數(shù)用于指明想要新線程執(zhí)行的線程函數(shù)的地址。線程函數(shù)的 p v P a r a m參數(shù)與原先傳遞給C r e a t e T h r e a d的p v P a r a m參數(shù)是相同的。C r e a t e T h r e a d使用該參數(shù)不做別的事情,只是在線程啟動(dòng)執(zhí)行時(shí)將該參數(shù)傳遞給線程函數(shù)。該參數(shù)提供了一個(gè)將初始化值傳遞給線程函數(shù)的手段。該初始化數(shù)據(jù)既可以是數(shù)字值,也可以是指向包含其他信息的一個(gè)數(shù)據(jù)結(jié)構(gòu)的指針。
創(chuàng)建多個(gè)線程,使這些線程擁有與起始點(diǎn)相同的函數(shù)地址,這是完全合乎邏輯的并且是非常有用的。例如,可以實(shí)現(xiàn)一個(gè)We b服務(wù)器,以便創(chuàng)建一個(gè)新線程來(lái)處理每個(gè)客戶(hù)機(jī)的請(qǐng)求。每個(gè)線程都知道它正在處理哪個(gè)客戶(hù)機(jī)的請(qǐng)求,因?yàn)楫?dāng)創(chuàng)建線程時(shí),你傳遞了一個(gè)不同的 p z P a r a m值。
記住,Wi n d o w s是個(gè)搶占式多線程系統(tǒng),這意味著新線程和調(diào)用 C r e a t e T h r e a d的線程可以同時(shí)執(zhí)行。由于線程可以同時(shí)運(yùn)行,就會(huì)出現(xiàn)一些問(wèn)題。請(qǐng)看下面的代碼:
?
? ? 在上面這個(gè)代碼中,F i r s t T h r e a d可以在S e c o n d T h r e a d將5分配給F i r s t T h r e a d的x之前結(jié)束它的操作。如果出現(xiàn)這種情況,S e c o n d T h r e a d將不知道F i r s t T h r e a d已經(jīng)不再存在,并且仍然試圖修改現(xiàn)在已經(jīng)是個(gè)無(wú)效地址的內(nèi)容。這會(huì)導(dǎo)致S e c o n d T h r e a d產(chǎn)生一次訪問(wèn)違規(guī),因?yàn)?/span>F i r s t T h r e a d的堆棧已經(jīng)在F i r s t T h r e a d終止運(yùn)行時(shí)被撤消。解決這個(gè)問(wèn)題的方法之一是將 x聲明為一個(gè)靜態(tài)變量,這樣,編譯器就為應(yīng)用程序的數(shù)據(jù)部分中的x創(chuàng)建一個(gè)存儲(chǔ)區(qū),而不是在堆棧上創(chuàng)建存儲(chǔ)區(qū)。
但是這使得函數(shù)成為不可重新進(jìn)入的函數(shù)。換句話說(shuō),無(wú)法創(chuàng)建兩個(gè)執(zhí)行相同函數(shù)的線程,因?yàn)閮蓚€(gè)線程將共享該靜態(tài)變量。解決這個(gè)問(wèn)題(和它的更復(fù)雜的變形)的另一種方法是使用正確的線程同步技術(shù)(第8、9章和1 0章介紹) 。
?
6.4.4 fdwCreate
????f d w C r e a t e參數(shù)可以設(shè)定用于控制創(chuàng)建線程的其他標(biāo)志。它可以是兩個(gè)值中的一個(gè)。如果該值是0,那么線程創(chuàng)建后可以立即進(jìn)行調(diào)度。如果該值是 C R E AT E _ S U S P E N D E D,系統(tǒng)可以完整地創(chuàng)建線程并對(duì)它進(jìn)行初始化,但是要暫停該線程的運(yùn)行,這樣它就無(wú)法進(jìn)行調(diào)度。
C R E AT E _ S U S P E N D E D標(biāo)志使得應(yīng)用程序能夠在它有機(jī)會(huì)執(zhí)行任何代碼之前修改線程的某些屬性。由于這種必要性很少,因此該標(biāo)志并不常用。第 5章介紹的J o b L a b應(yīng)用程序說(shuō)明了該標(biāo)志的正確方法。
6.4.5 pdwThreadID
? ? C r e a t e T h r e a d的最后一個(gè)參數(shù)是p d w T h r e a d I D,它必須是D W O R D的一個(gè)有效地址,C r e a t e T h r e a d使用這個(gè)地址來(lái)存放系統(tǒng)分配給新線程的 I D (進(jìn)程和線程的I D已經(jīng)在第4章中作了介紹)。
注意 在Windows 2000(和Windows NT 4)下,可以(并且通常是這樣做的)為該參數(shù)傳遞N U L L。它告訴函數(shù),你對(duì)線程的 I D不感興趣,但是線程已經(jīng)創(chuàng)建了。在Windows 95和Windows 98下,為該參數(shù)傳遞N U L L會(huì)導(dǎo)致函數(shù)運(yùn)行失敗,因?yàn)楹瘮?shù)試圖將I D寫(xiě)入地址N U L L(這是不合法的) 。因此線程不能創(chuàng)建。
當(dāng)然,操作系統(tǒng)之間的不一致現(xiàn)象會(huì)給編程人員帶來(lái)一些問(wèn)題。例如,在Wi n d o w s2 0 0 0下(即使為p d w T h r e a d I D參數(shù)傳遞了N U L L,它也創(chuàng)建了該線程)編寫(xiě)和測(cè)試了一個(gè)應(yīng)用程序,當(dāng)后來(lái)在Windows 98上運(yùn)行該應(yīng)用程序時(shí),C r e a t e T h r e a d將不創(chuàng)建新的線程。必須始終在你聲稱(chēng)支持的所有操作系統(tǒng)(和所有版本)上充分測(cè)試應(yīng)用程序。
?
6.5 終止線程的運(yùn)行
若要終止線程的運(yùn)行,可以使用下面的方法:
? 線程函數(shù)返回(最好使用這種方法) 。
? 通過(guò)調(diào)用E x i t T h r e a d函數(shù),線程將自行撤消(最好不要使用這種方法) 。
? 同一個(gè)進(jìn)程或另一個(gè)進(jìn)程中的線程調(diào)用Te r m i n a t e T h r e a d函數(shù)(應(yīng)該避免使用這種方法) 。
? 包含線程的進(jìn)程終止運(yùn)行(應(yīng)該避免使用這種方法) 。
下面將介紹終止線程運(yùn)行的方法,并且說(shuō)明線程終止運(yùn)行時(shí)會(huì)出現(xiàn)什么情況。
6.5.1 線程函數(shù)返回
????始終都應(yīng)該將線程設(shè)計(jì)成這樣的形式,即當(dāng)想要線程終止運(yùn)行時(shí),它們就能夠返回。這是確保所有線程資源被正確地清除的唯一辦法。
如果線程能夠返回,就可以確保下列事項(xiàng)的實(shí)現(xiàn):
? 在線程函數(shù)中創(chuàng)建的所有C + +對(duì)象均將通過(guò)它們的撤消函數(shù)正確地撤消。
? 操作系統(tǒng)將正確地釋放線程堆棧使用的內(nèi)存。
? 系統(tǒng)將線程的退出代碼(在線程的內(nèi)核對(duì)象中維護(hù))設(shè)置為線程函數(shù)的返回值。
? 系統(tǒng)將遞減線程內(nèi)核對(duì)象的使用計(jì)數(shù)。
6.5.2 ExitThread函數(shù)
????可以讓線程調(diào)用E x i t T h r e a d函數(shù),以便強(qiáng)制線程終止運(yùn)行:
VOID ExitThread(DWORD dwExitCode);
該函數(shù)將終止線程的運(yùn)行,并導(dǎo)致操作系統(tǒng)清除該線程使用的所有操作系統(tǒng)資源。但是,C + +資源(如C + +類(lèi)對(duì)象)將不被撤消。由于這個(gè)原因,最好從線程函數(shù)返回,而不是通過(guò)調(diào)用E x i t T h r e a d來(lái)返回(詳細(xì)說(shuō)明參見(jiàn)第4章) 。
????當(dāng)然,可以使用E x i t T h r e a d的d w E x i t T h r e a d參數(shù)告訴系統(tǒng)將線程的退出代碼設(shè)置為什么。E x i t T h r e a d函數(shù)并不返回任何值,因?yàn)榫€程已經(jīng)終止運(yùn)行,不能執(zhí)行更多的代碼。
注意 終止線程運(yùn)行的最佳方法是讓它的線程函數(shù)返回。但是,如果使用本節(jié)介紹的方法, 應(yīng)該知道E x i t T h r e a d函數(shù)是Wi n d o w s用來(lái)撤消線程的函數(shù)。如果編寫(xiě)C / C + +代碼,那么決不應(yīng)該調(diào)用E x i t T h r e a d。應(yīng)該使用Visual C++運(yùn)行期庫(kù)函數(shù)_ e n d t h r e a d e x。如果不使用M i c r o s o f t的Visual C++編譯器,你的編譯器供應(yīng)商有它自己的 E x i t T h r e a d的替代函數(shù)。不管這個(gè)替代函數(shù)是什么,都必須使用。本章后面將說(shuō)明 _ e n d t h r e a d e x的作用和它的重要性。
6.5.3 Te r m i n a t e T h r e a d函數(shù)
? ? 調(diào)用Te r m i n a t e T h r e a d函數(shù)也能夠終止線程的運(yùn)行:
?
? ? 與E x i t T h r e a d不同,E x i t T h r e a d總是撤消調(diào)用的線程, 而Te r m i n a t e T h r e a d能夠撤消任何線程。h T h r e a d參數(shù)用于標(biāo)識(shí)被終止運(yùn)行的線程的句柄。當(dāng)線程終止運(yùn)行時(shí),它的退出代碼成為你作為d w E x i t C o d e參數(shù)傳遞的值。同時(shí),線程的內(nèi)核對(duì)象的使用計(jì)數(shù)也被遞減。
? ? 注意 Te r m i n a t e T h r e a d函數(shù)是異步運(yùn)行的函數(shù),也就是說(shuō),它告訴系統(tǒng)你想要線程終止運(yùn)行,但是,當(dāng)函數(shù)返回時(shí),不能保證線程被撤消。如果需要確切地知道該線程已經(jīng)終止運(yùn)行,必須調(diào)用Wa i t F o r S i n g l e O b j e c t (第9章介紹)或者類(lèi)似的函數(shù),傳遞線程的句柄。
設(shè)計(jì)良好的應(yīng)用程序從來(lái)不使用這個(gè)函數(shù), 因?yàn)楸唤K止運(yùn)行的線程收不到它被撤消的通知。線程不能正確地清除,并且不能防止自己被撤消。
? ? 注意 當(dāng)使用返回或調(diào)用E x i t T h r e a d的方法撤消線程時(shí),該線程的內(nèi)存堆棧也被撤消。但是,如果使用Te r m i n a t e T h r e a d,那么在擁有線程的進(jìn)程終止運(yùn)行之前,系統(tǒng)不撤消該線程的堆棧。M i c r o s o f t故意用這種方法來(lái)實(shí)現(xiàn)Te r m i n a t e T h r e a d。如果其他仍然正在執(zhí)行的線程要引用強(qiáng)制撤消的線程堆棧上的值,那么其他的線程就會(huì)出現(xiàn)訪問(wèn)違規(guī)的問(wèn)題。如果將已經(jīng)撤消的線程的堆棧留在內(nèi)存中,那么其他線程就可以繼續(xù)很好地運(yùn)行。
此外,當(dāng)線程終止運(yùn)行時(shí), D L L通常接收通知。如果使用 Terminate Thread 強(qiáng)迫線程終止,D L L就不接收通知,這能阻止適當(dāng)?shù)那宄?#xff08;詳細(xì)信息參見(jiàn)第2 0章) 。
6.5.4 在進(jìn)程終止運(yùn)行時(shí)撤消線程
????第4章介紹的E x i t P r o c e s s和Te r m i n a t e P r o c e s s函數(shù)也可以用來(lái)終止線程的運(yùn)行。差別在于這些線程將會(huì)使終止運(yùn)行的進(jìn)程中的所有線程全部終止運(yùn)行。另外,由于整個(gè)進(jìn)程已經(jīng)被關(guān)閉,進(jìn)程使用的所有資源肯定已被清除。這當(dāng)然包括所有線程的堆棧。這兩個(gè)函數(shù)會(huì)導(dǎo)致進(jìn)程中的剩余線程被強(qiáng)制撤消,就像從每個(gè)剩余的線程調(diào)用 Te r m i n a t e T h r e a d一樣。顯然,這意味著正確
的應(yīng)用程序清除沒(méi)有發(fā)生,即C + +對(duì)象撤消函數(shù)沒(méi)有被調(diào)用,數(shù)據(jù)沒(méi)有轉(zhuǎn)至磁盤(pán)等等。
6.5.5 線程終止運(yùn)行時(shí)發(fā)生的操作
?當(dāng)線程終止運(yùn)行時(shí),會(huì)發(fā)生下列操作:
? 線程擁有的所有用戶(hù)對(duì)象均被釋放。在 Wi n d o w s中,大多數(shù)對(duì)象是由包含創(chuàng)建這些對(duì)象的線程的進(jìn)程擁有的。但是一個(gè)線程擁有兩個(gè)用戶(hù)對(duì)象,即窗口和掛鉤。當(dāng)線程終止運(yùn)行時(shí),系統(tǒng)會(huì)自動(dòng)撤消任何窗口,并且卸載線程創(chuàng)建的或安裝的任何掛鉤。其他對(duì)象只有在擁有線程的進(jìn)程終止運(yùn)行時(shí)才被撤消。
? 線程的退出代碼從S T I L L _ A C T I V E改為傳遞給E x i t T h r e a d或Te r m i n a t e T h r e a d的代碼。
? 線程內(nèi)核對(duì)象的狀態(tài)變?yōu)橐淹ㄖ?/p>
? 如果線程是進(jìn)程中最后一個(gè)活動(dòng)線程,系統(tǒng)也將進(jìn)程視為已經(jīng)終止運(yùn)行。
? 線程內(nèi)核對(duì)象的使用計(jì)數(shù)遞減1。當(dāng)一個(gè)線程終止運(yùn)行時(shí),在與它相關(guān)聯(lián)的線程內(nèi)核對(duì)象的所有未結(jié)束的引用關(guān)閉之前,該內(nèi)核對(duì)象不會(huì)自動(dòng)被釋放。
一旦線程不再運(yùn)行,系統(tǒng)中就沒(méi)有別的線程能夠處理該線程的句柄。然而別的線程可以調(diào)用G e t E x i t c o d e T h r e a d來(lái)檢查由h T h r e a d標(biāo)識(shí)的線程是否已經(jīng)終止運(yùn)行。如果它已經(jīng)終止運(yùn)行,則確定它的退出代碼:
?
????退出代碼的值在p d w E x i t C o d e指向的D W O R D中返回。如果調(diào)用G e t E x i t C o d e T h r e a d時(shí)線程尚未終止運(yùn)行,該函數(shù)就用 S T I L L _ A C T I V E標(biāo)識(shí)符(定義為0 x 1 0 3)填入D W O R D。如果該函數(shù)運(yùn)行成功,便返回 T R U E(第9章將詳細(xì)地介紹如何使用線程的句柄來(lái)確定何時(shí)線程終止運(yùn)行) 。
?
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專(zhuān)家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的Windows核心编程 第六章 线程基础知识 (上)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Intel汇编程序设计-整数算术指令(下
- 下一篇: Windows核心编程 第六章 线程基础