技术演进中的开发沉思-9:window编程系列-内核对象线程同步(下)
今天我们继续走进 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相关的问题,未完待续.........