今天我們繼續走進 Windows 內核的世界,就昨天沒說完的內核對象與線程同步內容接著繼續,它們就像精密儀器里的齒輪,雖不顯眼,卻至關重要。
異步設備 I/O
在 Windows 系統中,異步設備 I/O 就像是一場精心編排的接力賽。想象一下,我們的計算機系統是一個龐大的工廠,各個設備(比如硬盤、網卡)就是工廠里忙碌的工人,而應用程序則是負責下訂單的客戶。當應用程序需要從硬盤讀取數據時,如果采用同步 I/O,就好比客戶站在工廠門口,眼巴巴地等著工人把貨物一件件搬出來,在這個過程中,客戶什么都做不了,只能干等。而異步 I/O 則不同,它允許客戶下完訂單后,不用傻等,繼續去做其他事情,工廠(設備)在準備好貨物后,會主動通知客戶來取。
在 Windows 編程中,使用重疊 I/O(一種異步 I/O 方式)來實現這個過程。下面是一段簡單的 VC++ 代碼示例,展示如何使用異步 I/O 從文件中讀取數據:
#include <windows.h>#include <stdio.h>int main() {HANDLE hFile = CreateFile(TEXT("test.txt"),GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,NULL);if (hFile == INVALID_HANDLE_VALUE) {printf("Failed to open file. Error: %d\n", GetLastError());return 1;}OVERLAPPED overlapped = { 0 };overlapped.Offset = 0;overlapped.OffsetHigh = 0;overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);DWORD bytesRead;if (!ReadFile(hFile, NULL, 0, &bytesRead, &overlapped)) {if (GetLastError() != ERROR_IO_PENDING) {printf("ReadFile failed. Error: %d\n", GetLastError());CloseHandle(hFile);CloseHandle(overlapped.hEvent);return 1;}}// 可以在等待數據讀取完成的過程中做其他事情// 當數據讀取完成,事件會被觸發WaitForSingleObject(overlapped.hEvent, INFINITE);CloseHandle(hFile);CloseHandle(overlapped.hEvent);return 0;}
在這段代碼里,CreateFile函數打開文件時設置了FILE_FLAG_OVERLAPPED標志,開啟異步模式。ReadFile函數在數據未準備好時立即返回,我們通過等待overlapped.hEvent事件來得知數據是否讀取完成。這樣,程序就不會在讀取數據時卡住,而是可以高效地利用時間,處理其他任務,就像接力賽中,下一棒選手可以提前做好準備,而不是傻傻地站在原地等待。
二、WaitForInputIdle 函數
WaitForInputIdle函數就像是一位耐心的管家。在 Windows 系統中,當我們啟動一個新的進程,比如打開一個應用程序時,這個程序可能需要一些時間來初始化,加載資源、設置窗口布局等等。在這個過程中,如果我們立即對它進行操作,可能會出現混亂,就好比一個剛起床還沒收拾好的人,你馬上讓他去接待客人,肯定會手忙腳亂。
WaitForInputIdle函數的作用就是讓我們等待程序完成初始化,準備好接收用戶輸入后,再進行后續操作。它就像管家在門口守著,告訴我們:“先別著急進去打擾,等里面準備好了,我再通知你。” 以下是一個簡單的使用示例:
#include <windows.h>#include <stdio.h>int main() {SHELLEXECUTEINFO ShExecInfo = { 0 };ShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO);ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS;ShExecInfo.hwnd = NULL;ShExecInfo.lpVerb = NULL;ShExecInfo.lpFile = TEXT("notepad.exe");ShExecInfo.lpParameters = NULL;ShExecInfo.lpDirectory = NULL;ShExecInfo.nShow = SW_NORMAL;ShExecInfo.hInstApp = NULL;if (ShellExecuteEx(&ShExecInfo)) {// 等待記事本程序準備好接收用戶輸入WaitForInputIdle(ShExecInfo.hProcess, INFINITE);printf("Notepad is ready for input.\n");// 可以在這里添加對記事本的操作代碼CloseHandle(ShExecInfo.hProcess);} else {printf("Failed to launch Notepad. Error: %d\n", GetLastError());}return 0;}
在這段代碼中,我們使用ShellExecuteEx函數啟動記事本程序,然后調用WaitForInputIdle函數等待記事本完成初始化。只有當記事本準備就緒,程序才會繼續執行后續操作,這就避免了因過早操作而可能引發的問題,讓整個過程更加順暢、有序。
三、MsgWaitForMultipleObjects(ex)函數
MsgWaitForMultipleObjects(ex)函數就像是一個忙碌的交通指揮員,它負責管理多個內核對象和消息隊列。在 Windows 系統中,我們的程序可能會創建多個線程,每個線程可能有自己的任務,同時,程序還需要處理各種消息(比如用戶的鼠標點擊、鍵盤輸入)。這些線程和消息就像道路上川流不息的車輛,如果沒有一個好的指揮,很容易造成混亂和擁堵。
MsgWaitForMultipleObjects(ex)函數可以同時等待多個內核對象(比如事件、信號量)變為有信號狀態,并且在等待過程中,還能處理消息隊列中的消息。它會根據不同的情況,決定是繼續等待內核對象,還是先處理消息,就像交通指揮員根據道路情況,靈活地指揮車輛通行,保證整個系統的流暢運行。下面是一個簡單的示例代碼:
#include <windows.h>#include <stdio.h>DWORD WINAPI ThreadProc(LPVOID lpParameter) {// 模擬線程執行任務for (int i = 0; i < 5; ++i) {printf("Thread is working...\n");Sleep(1000);}// 線程完成任務后設置事件HANDLE hEvent = (HANDLE)lpParameter;SetEvent(hEvent);return 0;}int main() {HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);HANDLE hThread = CreateThread(NULL, 0, ThreadProc, (LPVOID)hEvent, 0, NULL);DWORD result = MsgWaitForMultipleObjects(1,&hEvent,FALSE,INFINITE,QS_ALLINPUT);if (result == WAIT_OBJECT_0) {printf("Thread completed its task.\n");} else {printf("Error occurred. Result: %d\n", result);}CloseHandle(hThread);CloseHandle(hEvent);return 0;}
在這個示例中,我們創建了一個線程和一個事件。MsgWaitForMultipleObjects函數等待事件hEvent變為有信號狀態,同時在等待過程中,它還會處理消息隊列中的消息。當線程完成任務并設置事件后,MsgWaitForMultipleObjects函數就會返回,程序繼續執行后續操作,整個過程有條不紊,就像交通指揮員讓車輛順利通過繁忙的路口。
四、WaitForDebugEvent 函數
WaitForDebugEvent函數就像一位嚴謹的質檢員,專門負責監控和調試程序的運行狀態。在軟件開發過程中,程序難免會出現各種問題,就像生產線上的產品可能會有瑕疵。WaitForDebugEvent函數可以幫助我們捕獲程序運行時的各種事件(比如斷點命中、異常拋出),就像質檢員仔細檢查每一個產品,不放過任何一個可能存在的問題。
當我們使用調試器調試程序時,WaitForDebugEvent函數會等待調試事件的發生。一旦有調試事件出現,它就會通知調試器進行相應的處理,比如暫停程序執行、查看變量值等。以下是一個簡單的調試示例代碼框架:
#include <windows.h>#include <stdio.h>int main() {STARTUPINFO si = { sizeof(si) };PROCESS_INFORMATION pi;if (!CreateProcess(NULL,TEXT("test.exe"),NULL,NULL,FALSE,DEBUG_PROCESS,NULL,NULL,&si,&pi)) {printf("Failed to create process. Error: %d\n", GetLastError());return 1;}DEBUG_EVENT debugEvent;while (WaitForDebugEvent(&debugEvent, INFINITE)) {// 處理調試事件switch (debugEvent.dwDebugEventCode) {case EXCEPTION_DEBUG_EVENT:// 處理異常事件break;case CREATE_PROCESS_DEBUG_EVENT:// 處理進程創建事件break;// 其他類型的調試事件處理default:break;}ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);}CloseHandle(pi.hProcess);CloseHandle(pi.hThread);return 0;}
在這段代碼中,我們使用CreateProcess函數以調試模式啟動一個程序(這里假設為test.exe),然后通過WaitForDebugEvent函數循環等待調試事件的發生。一旦捕獲到調試事件,就根據事件類型進行相應的處理,處理完后使用ContinueDebugEvent函數讓程序繼續執行。這就像質檢員發現產品問題后,進行記錄和處理,確保產品質量符合要求,幫助開發者找到并解決程序中的問題。
五、SignalObjectAndWait 函數
SignalObjectAndWait函數就像一位默契的橋梁搭建者,它在兩個內核對象之間建立起一種特殊的聯系,實現原子操作。想象一下,有兩個任務,一個任務完成后需要通知另一個任務開始執行,同時還要確保在通知的過程中,不會出現其他干擾,保證整個過程的原子性(即要么都完成,要么都不完成)。
SignalObjectAndWait函數可以先將一個內核對象(比如事件)設置為有信號狀態,然后立即等待另一個內核對象變為有信號狀態。在這個過程中,它會確保設置信號和等待信號這兩個操作是連續進行的,不會被其他線程打斷。以下是一個示例代碼:
#include <windows.h>#include <stdio.h>int main() {HANDLE hEvent1 = CreateEvent(NULL, FALSE, FALSE, NULL);HANDLE hEvent2 = CreateEvent(NULL, FALSE, FALSE, NULL);// 啟動一個線程,等待hEvent1變為有信號狀態HANDLE hThread = CreateThread(NULL, 0, [](LPVOID lpParameter) -> DWORD {WaitForSingleObject((HANDLE)lpParameter, INFINITE);printf("Thread received signal and started working.\n");// 線程完成任務后設置hEvent2為有信號狀態SetEvent((HANDLE)((DWORD_PTR)lpParameter + 1));return 0;}, (LPVOID)hEvent1, 0, NULL);// 主線程等待一段時間后,使用SignalObjectAndWait函數Sleep(2000);SignalObjectAndWait(hEvent1, hEvent2, INFINITE, FALSE);printf("Main thread completed the operation.\n");CloseHandle(hThread);CloseHandle(hEvent1);CloseHandle(hEvent2);return 0;}
在這個示例中,主線程使用SignalObjectAndWait函數先將hEvent1設置為有信號狀態,通知線程開始執行任務,然后等待hEvent2變為有信號狀態,即等待線程完成任務。整個過程通過SignalObjectAndWait函數實現了任務之間的有序協作,就像橋梁搭建者在兩個地點之間建起一座穩固的橋梁,讓任務的傳遞和執行更加順暢、可靠。
六、使用等待鏈遍歷 API 來檢測死鎖
在多線程編程中,死鎖是一個令人頭疼的問題,就像道路上車輛相互卡住,誰也動不了,導致整個系統陷入僵局。而使用等待鏈遍歷 API 來檢測死鎖,就像一位敏銳的故障偵探,能夠及時發現這些潛在的問題。
死鎖通常發生在多個線程互相等待對方釋放資源的情況下。等待鏈遍歷 API 可以幫助我們檢查線程之間的等待關系,通過分析等待鏈,找出是否存在循環等待的情況,從而判斷是否發生了死鎖。下面是一個簡單的死鎖檢測示例代碼框架(實際應用中會更復雜):
#include <windows.h>#include <stdio.h>// 模擬兩個線程競爭資源可能導致死鎖的情況DWORD WINAPI Thread1Proc(LPVOID lpParameter) {HANDLE hMutex1 = (HANDLE)((DWORD_PTR)lpParameter);HANDLE hMutex2 = (HANDLE)((DWORD_PTR)lpParameter + 1);WaitForSingleObject(hMutex1, INFINITE);Sleep(1000);WaitForSingleObject(hMutex2, INFINITE);ReleaseMutex(hMutex2);ReleaseMutex(hMutex1);return 0;}DWORD WINAPI Thread2Proc(LPVOID lpParameter) {HANDLE hMutex1 = (HANDLE)((DWORD_PTR)lpParameter);HANDLE hMutex2 = (HANDLE)((DWORD_PTR)lpParameter + 1);WaitForSingleObject(hMutex2, INFINITE);Sleep(1000);WaitForSingleObject(hMutex1, INFINITE);ReleaseMutex(hMutex1);ReleaseMutex(hMutex2);return 0;}int main() {HANDLE hMutex1 = CreateMutex(NULL, FALSE, NULL);HANDLE hMutex2 = CreateMutex(NULL, FALSE, NULL);HANDLE hThread1 = CreateThread(NULL, 0, Thread1Proc, (LPVOID)hMutex1, 0, NULL);HANDLE hThread2 = CreateThread(NULL, 0, Thread2Proc, (LPVOID)hMutex1, 0, NULL);// 模擬等待一段時間后進行死鎖檢測Sleep(3000);// 這里可以使用等待鏈遍歷API進行死鎖檢測,實際代碼會更復雜// 為簡化說明,暫不展開具體檢測代碼CloseHandle(hThread1);CloseHandle(hThread2);CloseHandle(hMutex1);CloseHandle(hMutex2);return 0;}
在這個示例中,兩個線程Thread1Proc和Thread2Proc以不同的順序獲取互斥鎖hMutex1和hMutex2,很可能會導致死鎖。在實際應用中,我們可以使用等待鏈遍歷 API 來檢測線程之間的等待關系,一旦發現存在循環等待的情況,就可以判斷發生了死鎖,并及時采取措施進行處理,就像偵探發現案件線索后,迅速展開調查并解決問題,保證系統的正常運行。
最后小結:
在我眼里,異步設備 I/O 如接力賽,通過重疊 I/O 實現異步讀取,讓程序在等待數據時能處理其他任務;WaitForInputIdle函數像耐心管家,確保新啟動程序完成初始化后再接收操作,避免混亂;MsgWaitForMultipleObjects(ex)函數是忙碌的交通指揮員,兼顧多個內核對象與消息隊列,維持系統運行秩序 。?
WaitForDebugEvent函數如同嚴謹質檢員,在程序調試時捕獲各類事件,助力開發者定位問題;SignalObjectAndWait函數是默契的橋梁搭建者,實現內核對象間原子操作,保障任務有序協作;等待鏈遍歷 API 則像敏銳的故障偵探,通過分析線程等待關系檢測死鎖,保障系統穩定。今天的內容就到這里吧!下一節,我們將梳理一下windows中很重要I/O相關的問題,未完待續.........