技术演进中的开发沉思-9:window编程系列-内核对象线程同步(下)

今天我们继续走进 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相关的问题,未完待续.........

相关推荐
love530love3 小时前
【笔记】解决部署国产AI Agent 开源项目 MiniMax-M1时 Hugging Face 模型下载缓存占满 C 盘问题:更改缓存位置全流程
开发语言·人工智能·windows·笔记·python·缓存·uv
程序员的世界你不懂6 小时前
Windows下allure与jenkins的集成
windows·servlet·jenkins
方博士AI机器人7 小时前
GNU Octave 基础教程(4):变量与数据类型详解(二)
windows·ubuntu·数据分析·octave
你们补药再卷啦13 小时前
windows,java后端开发常用软件的下载,使用配置
windows
板栗栗-714 小时前
Windows系统提示“mfc140u.dll丢失”?详细修复指南,一键恢复程序运行!
windows·dll·dll修复·dll错误·dll缺失
love530love16 小时前
Python 开发环境全栈隔离架构:从 Anaconda 到 PyCharm 的四级防护体系
运维·ide·人工智能·windows·python·架构·pycharm
fo安方17 小时前
运维的利器–监控–zabbix–第二步:建设–部署zabbix agent5.0--客户端是windows&centos系统--实操记录gys
运维·windows·zabbix
草明17 小时前
Python pip 以及 包的升级
windows·python·pip
小馒头君君20 小时前
近期GitHub热榜推荐
开发语言·windows·python·学习·github