技术演进中的开发沉思-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相关的问题,未完待续.........

相关推荐
武藤一雄21 小时前
C# 关于多线程如何实现需要注意的问题(持续更新)
windows·后端·microsoft·c#·.net·.netcore·死锁
coding消烦员1 天前
在 Windows 内网搭建 Git 仓库:共享普通仓库 vs 中心 bare 仓库
windows·git
xiaoliuliu123451 天前
IE8-WindowsXP-x86-CHS_23253_BDdl.exe 安装步骤(XP 32位简体中文版)
windows
百事牛科技1 天前
文件不想再加密了?取消Word 打开密码的方法
windows·word
love530love1 天前
EPGF 新手教程 13在 PyCharm(中文版 GUI)中创建 Hatch 项目环境,并把 Hatch 做成“项目自包含”(工具本地化为必做环节)
开发语言·ide·人工智能·windows·python·pycharm·hatch
峰上踏雪1 天前
Go(Golang)Windows 环境配置关键点总结
开发语言·windows·golang·go语言
lusasky1 天前
在Windows上编译、安装Rust
开发语言·windows·rust
麻辣长颈鹿Sir1 天前
CMAKE指令集
linux·运维·windows·cmake·cmake指令集
Alice10291 天前
如何在windows本地打包python镜像
开发语言·windows·python
北京流年1 天前
windows安装jenkins并且编译app
运维·windows·jenkins