今天我们继续走进 Windows 内核的世界,就昨天没说完的内核对象与线程同步内容接着继续,它们就像精密仪器里的齿轮,虽不显眼,却至关重要。

异步设备 I/O
在 Windows 系统中,异步设备 I/O 就像是一场精心编排的接力赛。想象一下,我们的计算机系统是一个庞大的工厂,各个设备(比如硬盘、网卡)就是工厂里忙碌的工人,而应用程序则是负责下订单的客户。当应用程序需要从硬盘读取数据时,如果采用同步 I/O,就好比客户站在工厂门口,眼巴巴地等着工人把货物一件件搬出来,在这个过程中,客户什么都做不了,只能干等。而异步 I/O 则不同,它允许客户下完订单后,不用傻等,继续去做其他事情,工厂(设备)在准备好货物后,会主动通知客户来取。
在 Windows 编程中,使用重叠 I/O(一种异步 I/O 方式)来实现这个过程。下面是一段简单的 VC++ 代码示例,展示如何使用异步 I/O 从文件中读取数据:
cpp
#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函数的作用就是让我们等待程序完成初始化,准备好接收用户输入后,再进行后续操作。它就像管家在门口守着,告诉我们:"先别着急进去打扰,等里面准备好了,我再通知你。" 以下是一个简单的使用示例:
cpp
#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)函数可以同时等待多个内核对象(比如事件、信号量)变为有信号状态,并且在等待过程中,还能处理消息队列中的消息。它会根据不同的情况,决定是继续等待内核对象,还是先处理消息,就像交通指挥员根据道路情况,灵活地指挥车辆通行,保证整个系统的流畅运行。下面是一个简单的示例代码:
cpp
#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函数会等待调试事件的发生。一旦有调试事件出现,它就会通知调试器进行相应的处理,比如暂停程序执行、查看变量值等。以下是一个简单的调试示例代码框架:
cpp
#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函数可以先将一个内核对象(比如事件)设置为有信号状态,然后立即等待另一个内核对象变为有信号状态。在这个过程中,它会确保设置信号和等待信号这两个操作是连续进行的,不会被其他线程打断。以下是一个示例代码:
cpp
#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 可以帮助我们检查线程之间的等待关系,通过分析等待链,找出是否存在循环等待的情况,从而判断是否发生了死锁。下面是一个简单的死锁检测示例代码框架(实际应用中会更复杂):
cpp
#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相关的问题,未完待续.........