《TCP/IP網絡編程》學習筆記 | Chapter 19:Windows 平臺下線程的使用
- 《TCP/IP網絡編程》學習筆記 | Chapter 19:Windows 平臺下線程的使用
- 內核對象
- 內核對象的定義
- 內核對象歸操作系統所有
- 基于 Windows 的線程創建
- 進程與線程的關系
- Windows 中線程的創建方法
- Windows 線程的銷毀時間點
- 編寫多線程程序的環境設置
- 創建“使用線程安全標準 C 函數”的線程
- 句柄、內核對象和 ID 間的關系
- 內核對象的 2 種狀態
- 內核對象狀態及狀態查看
- 驗證內核對象狀態的 2 個函數
- 習題
- (1)下列關于內核對象的說法錯誤的是?
- (2)現代操作系統大部分都在操作系統級別支持線程。根據該情況判斷下列描述中錯誤的是?
- (3)請比較從內存中完全銷毀 Windows 線程和 Linux 線程的方法。
- (4)通過線程創建過程解釋內核對象、線程、句柄之間的關系。
- (5)判斷下列關于內核對象描述的正誤。
- (6)請解釋“auto-reset模式”和manual-reset模式”的內核對象。區分二者的內核對象特征是什么?
《TCP/IP網絡編程》學習筆記 | Chapter 19:Windows 平臺下線程的使用
內核對象
要想掌握 Windows 平臺下的線程,應首先理解內核對象(Kernel Objects)的概念。
內核對象的定義
操作系統創建的資源有很多種,如進程、線程、文件及即將介紹的信號量、互斥量等。其中大部分都是通過程序員的請求創建的,而且請求方式各不相同。雖然存在一些差異,但它們之間也有如下共同點:都是由 Windows 操作系統創建并管理的資源。
不同資源類型在管理方式也有差異。例如,文件管理中應注冊并更新文件相關的數據I/O位置、文件的打開模式(rcad or write)等。如果是線程,則應注冊并維護線程ID、線程所屬進程等信息。操作系統為了以記錄相關信息的方式管理各種資源,在其內部生成數據塊(結構體變量)。當然,每種資源需要維護的信息不同,所以每種資源擁有的數據塊格式也有差異。這類數據塊稱為“內核對象”。
假設在 Windows 下創建了 mydata.txt 文件,此時 Windows 操作系統將生成 1 個數據塊以便管理,該數據塊就是內核對象。同理,Windows 在創建進程、線程、線程同步信號量時也會生成相應的內核對象,用于管理操作系統資源。
內核對象歸操作系統所有
線程、文件等資源的創建請求均在進程內部完成,因此,很容易產生“此時創建的內核對象所有者就是進程”的錯覺。其實,內核對象所有者是內核(操作系統),內核對象的創建、管理、銷毀時機的決定等工作均由操作系統完成。
基于 Windows 的線程創建
進程與線程的關系
現代操作系統都支持線程,因此,非顯式創建線程的程序可描述為“單一線程模型的應用程序”,顯式創建單獨線程的程序可描述為“多線程模型的應用程序”。這就意味著 main 函數的運行同樣基于線程完成,此時進程可以比喻為裝有線程的籃子,實際的運行主體是線程。
Windows 中線程的創建方法
#include <windows.h>HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,SIZE_T dwStackSize,LPTHREAD_START_ROUTINE lpStartAddress,LPVOID lpParameter,DWORD dwCreationFlags,LPDWORD lpThreadId
);
參數:
- IpThreadAttributes:線程安全相關信息,使用默認設置時傳遞 NULL。
- dwStackSize:要分配給線程的棧大小,傳遞 0 時生成默認大小的棧。
- IpStartAddress:傳遞線程的 main 函數信息。
- lpParameter:調用 main 函數時傳遞的參數信息。
- dwCreationFlags:用于指定線程創建后的行為,傳遞 0 時,線程創建后立即進入可執行狀態。
- IpThreadld:用于保存線程 ID 的變量地址值。
成功時返回線程句柄,失敗時返回 NULL。
調用該函數將創建線程,操作系統為了管理這些資源也將同時創建內核對象。最后返回用于區分內核對象的整數型“句柄”(Handle)。
第 1 章已介紹過,句柄相當于 Linux的文件描述符。
上述定義看起來有些復雜,其實只需要考慮 IpStartAddress 和 lpParameter 這 2 個參數,剩下的只需傳遞 0 或 NULL 即可。
Windows 線程的銷毀時間點
Windows 線程在首次調用的線程 main 函數返回時銷毀(銷毀時間點和銷毀方法與 Linux 不同)。
還有其他方法可以終止線程,但最好的方法就是讓線程main函數終止(返回)。
編寫多線程程序的環境設置
舊的 VC++6.0版默認只包含支持單線程的庫,需要自行配置。
在項目的屬性-代碼生成界面,可以檢查運行庫:
創建“使用線程安全標準 C 函數”的線程
通過 CreateThread 函數調用創建出的線程在使用 C/C++ 標準函數時并不穩定。
如果線程要調用 C/C++ 標準函數,需要通過如下方法創建線程:
#include <process.h>unitptr_t _beginthreadex(void *security,unsigned stack_size,unsigned (*start_address)(void *),void *arglist,unsigned initflag,unsigned *thrdaddr
);
上述函數與之前的 CreateThread 函數相比,參數個數及各參數的含義和順序的相同,只是變量名和參數類型有所不同。因此,用上述函數替換 CreateThread 函數時,只需適當更改數據類型。上述函數的返回值類型 uintptr_t 是 64 位 unsigned 整數型。
程序示例:
在這里插入代碼片
運行結果:
與 Linux 相同,Windows 同樣在 main 函數返回后終止進程,也同時終止其中包含的所有線程。另外,如果對上述代碼進行運行的話,最后輸出的內容并非字符串"end of main",而是"running thread"。但這是在 main 函數返回后,完全銷毀進程前輸出的字符串。
句柄、內核對象和 ID 間的關系
線程屬于操作系統管理資源,伴隨內核對象的創建,為了引用內核對象而返回句柄。
可以通過句柄區分內核對象,通過內核對象可以區分線程,最終,線程句柄成為區分線程的工具。
句柄的整數值在不同進程中可能重復。通過 CreateThread/_beginthreadex 函數可以得到線程 ID,它用于區分操作系統創建的所有線程,在跨進程范圍內不會重復。
內核對象的 2 種狀態
應用程序實現過程中需要特別關注的信息被賦予某種“狀態”。
線程終止狀態又稱 signaled 狀態,未終止狀態稱為 non-signaled 狀態。
內核對象狀態及狀態查看
進程或線程終止時,操作系統會把相應的內核對象改為 signaled 狀態。
這也意味著,進程和線程的內核對象初始狀態是 non-signaled 狀態。
內核對象帶有 1 個 boolean 變量,其初始值為 FALSE,此時的狀態就是 non-signaled 狀態。如果發生約定的情況,把該變量改為 TRUE,此時的狀態就是 signaled 狀態。內核對象類型不同,進入 signaled 狀態的情況也有所區別(即對應事件也有區別)。
驗證內核對象狀態的 2 個函數
首先介紹 WaitForSingleObject 函數,該函數針對單個內核對象驗證 signaled 狀態。
#include<windows.h>DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds
);
參數:
- hHandle:查看狀態的內核對象句柄。
- dwMilliseconds:以 1ms 為單位指定超時,傳遞 INFINITE 時函數不會返回,直到內核對象變成 signaled狀態。
進入 signaled 狀態返回 WAIT_OBJECT_0,超時返回 WAIT_TIMEOUT,失敗時返回 WAIT_FAILED。
該函數由于發生事件(變為 signaled 狀態)返回時,有時會把相應內核對象再次改為 non-signaled 狀態。這種可以再次進入 non-signaled 狀態的內核對象稱為“auto-reset模式”的內核對象,而不會自動跳轉到 non-signaled 狀態的內核對象稱為“manual-reset模式”的內核對象。
WaitForMultipleObjects 函數與 WaitForSingleObject 函數不同,可以驗證多個內核對象狀態。
#include <windows.h>DWORD WaitForMultipleObjects(DWORD nCount,const HANDLE *lphHandles,BOOL bWaitAll,DWORD dwMilliseconds
);
參數:
- nCount:需驗證的內核對象數。
- IpHandles:存有內核對象句柄的數組地址值。
- bWaitAll:如果為 TRUE,則所有內核對象全部變為 signaled 時返回;如果為 FALSE,則只要有 1 個對象的狀態變為 signaled 就會返回。
- dwMilliseconds:以 1ms 為單指定超時,傳遞 INFINITE 時函數不會返回,直到內核對象變為 signaled 狀態。
成功時返回事件信息,失敗時返回 WAIT_FAILED。
下面利用 WaitForSingleObject 函數嘗試解決示例的問題。
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h>unsigned WINAPI ThreadFunc(void *arg); // WINAPI 是 Windows 固有的關鍵字,表示遵守 _beginthreadex 函數的調用規定int main(int argc, char *argv[])
{HANDLE hThread;DWORD wr;unsigned threadID;int param = 5;hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void *)¶m, 0, &threadID);if (hThread == 0){puts("_beginthreadex() error");return -1;}if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED){puts("thread wait error");return -1;}printf("wait result: %s \n", (wr == WAIT_OBJECT_0) ? "signaled" : "time-out");puts("end of main");system("pause");return 0;
}unsigned WINAPI ThreadFunc(void *arg)
{int cnt = *((int *)arg);for (int i = 0; i < cnt; i++){Sleep(1000);puts("runnning thread");}return 0;
}
運行結果:
可以看出,thread1_win.c 中的問題得到解決。
第 18 章在 Linux 平臺下分析了臨界區問題,本章最后的內容將留給 Windows 平臺下的臨界區問題。
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h>#define NUM_THREAD 50
unsigned WINAPI threadInc(void *arg);
unsigned WINAPI threadDes(void *arg);
long long num = 0;int main(int argc, char *argv[])
{HANDLE tHandles[NUM_THREAD];int i;printf("sizeof long long: %d \n", sizeof(long long));for (i = 0; i < NUM_THREAD; i++){if (i % 2)tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);elsetHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);}WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);printf("result: %lld \n", num);system("pause");return 0;
}unsigned WINAPI threadInc(void *arg)
{int i;for (i = 0; i < 50000000; i++)num += 1;return 0;
}unsigned WINAPI threadDes(void *arg)
{int i;for (i = 0; i < 50000000; i++)num -= 1;return 0;
}
運行結果:
即使多運行幾次也無法得到正確結果,而且每次結果都不同。可以利用第 20 章的同步技術得到預想的結果。
習題
(1)下列關于內核對象的說法錯誤的是?
a. 內核對象是操作系統保存各種資源信息的數據塊。
b. 內核對象的所有者是創建該內核對象的進程。
c. 由用戶進程創建并管理內核對象。
d. 無論操作系統創建和管理的資源類型是什么,內核對象的數據塊結構都完全相同。
答:
b、c、d。
(2)現代操作系統大部分都在操作系統級別支持線程。根據該情況判斷下列描述中錯誤的是?
a. 調用 main 函數的也是線程。
b. 如果進程不創建線程,則進程內不存在任何線程。
c. 多線程模型是進程內可以創建額外線程的程序類型。
d. 單一線程模型是進程內只額外創建 1 個線程的程序模型。
答:
b、d。
(3)請比較從內存中完全銷毀 Windows 線程和 Linux 線程的方法。
(4)通過線程創建過程解釋內核對象、線程、句柄之間的關系。
線程創建過程:
- 用戶程序通過系統調用請求創建線程。
- 內核創建線程對象。
- 內核將線程對象的引用封裝為句柄,返回給用戶程序。
- 線程開始執行用戶定義的 main 函數,代碼運行在用戶態。
- 當線程函數結束且所有句柄關閉,內核銷毀線程對象。
總結:
內核對象是操作系統的核心資源管理者,線程是用戶程序與內核協作的執行單元,句柄是用戶程序安全訪問內核對象的橋梁。三者通過線程創建過程緊密協作,確保資源的隔離性、安全性和高效管理。
(5)判斷下列關于內核對象描述的正誤。
- 內核對象只有 signaled 和 non- signaled 這 2 種狀態。(×)
- 內核對象需要轉為 signaled 狀態時,需要程序員親自將內核對象的狀態改為 signaled 狀態。(×)
- 線程的內核對象在線程運行時處于 sigaled 狀態,線程終止則進入 non-signaled 狀態。(×)
(6)請解釋“auto-reset模式”和manual-reset模式”的內核對象。區分二者的內核對象特征是什么?
auto-reset 模式:當事件被觸發,只有一個等待線程會被喚醒,隨后事件自動重置為 non-signaled 狀態。如果有多個線程在等待,只有一個線程能繼續執行,其余線程繼續等待。
manual-reset 模式:當事件被觸發,所有等待線程都會被喚醒,事件保持 signaled 狀態,直到手動重置為 non- signaled 狀態。
選擇哪種模式取決于具體需求:是喚醒單個線程還是多個線程。