Windows中的I/O完成通知与事件内核对象

Windows中的I/O完成通知与事件内核对象

Windows提供了一套灵活机制来处理异步I/O操作,其中事件内核对象是较早期但至今仍有价值的通知方式之一。这种方式让程序能够发起I/O请求后继续执行其他任务,当I/O操作完成时,通过事件对象来通知调用线程。

基本原理

在同步I/O中,当线程调用读取或写入操作时,它会一直等待直到操作完成。异步I/O改变了这个模式:线程发起请求后立即返回,I/O操作在后台进行。为了知道操作何时完成,程序需要某种通知机制,事件内核对象就是其中一种选择。

系统在I/O操作完成时会设置事件对象为"有信号"状态,等待这个事件的线程就会被唤醒。这种方式特别适合那些需要同时处理多个I/O请求的场景,程序可以用WaitForMultipleObjects等待一组事件,任何一个I/O完成都可以立即处理。

使用模式

通过事件内核对象接收I/O完成通知通常遵循这样的步骤:准备重叠结构、发起异步操作、等待事件、获取结果。

c 复制代码
HANDLE hFile = CreateFile(
    TEXT("example.dat"),
    GENERIC_READ,
    FILE_SHARE_READ,
    NULL,
    OPEN_EXISTING,
    FILE_FLAG_OVERLAPPED,  // 关键:使用重叠I/O
    NULL
);

if (hFile == INVALID_HANDLE_VALUE) {
    // 错误处理
    return;
}

// 准备重叠结构和事件
OVERLAPPED overlapped = {0};
overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (overlapped.hEvent == NULL) {
    CloseHandle(hFile);
    return;
}

BYTE buffer[4096];
DWORD bytesRead = 0;

// 发起异步读取
BOOL result = ReadFile(
    hFile,           // 文件句柄
    buffer,          // 数据缓冲区
    sizeof(buffer),  // 要读取的字节数
    &bytesRead,      // 实际读取的字节数(异步时通常为NULL)
    &overlapped      // 重叠结构
);

if (!result) {
    DWORD error = GetLastError();
    if (error != ERROR_IO_PENDING) {
        // 不是"操作进行中"错误,表示真的出错了
        CloseHandle(overlapped.hEvent);
        CloseHandle(hFile);
        return;
    }
    
    // 错误是ERROR_IO_PENDING,表示操作正在异步进行
    // 这是正常情况
} else {
    // 如果ReadFile立即成功(比如数据已在缓存中)
    // 事件可能已经处于有信号状态,或即将被设置
}

// 等待I/O完成
DWORD waitResult = WaitForSingleObject(overlapped.hEvent, INFINITE);
if (waitResult == WAIT_OBJECT_0) {
    // 获取操作结果
    BOOL ioSucceeded = GetOverlappedResult(
        hFile,
        &overlapped,
        &bytesRead,
        FALSE  // 不等待
    );
    
    if (ioSucceeded) {
        // 处理读取到的数据
        printf("成功读取 %lu 字节\n", bytesRead);
    } else {
        // I/O操作失败
        error = GetLastError();
        printf("I/O失败,错误代码: %lu\n", error);
    }
} else {
    // 等待失败
    printf("等待事件失败\n");
}

// 清理
CloseHandle(overlapped.hEvent);
CloseHandle(hFile);

处理多个并发I/O

事件内核对象的真正优势在于能同时管理多个I/O操作。通过创建多个事件对象,程序可以并发发起多个请求,然后统一等待它们完成。

c 复制代码
#define MAX_OPERATIONS 5

HANDLE hEvents[MAX_OPERATIONS];
OVERLAPPED overlappedArray[MAX_OPERATIONS];
BYTE buffers[MAX_OPERATIONS][1024];
HANDLE hFile;

// 初始化:为每个操作创建事件
for (int i = 0; i < MAX_OPERATIONS; i++) {
    overlappedArray[i].Offset = i * 1024;  // 从不同位置读取
    overlappedArray[i].OffsetHigh = 0;
    overlappedArray[i].hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    hEvents[i] = overlappedArray[i].hEvent;
}

// 并发发起多个读取请求
for (int i = 0; i < MAX_OPERATIONS; i++) {
    ReadFile(hFile, buffers[i], 1024, NULL, &overlappedArray[i]);
    // 注意:这里不检查返回值,因为ERROR_IO_PENDING是预期结果
}

// 等待所有操作完成
DWORD completedCount = 0;
while (completedCount < MAX_OPERATIONS) {
    DWORD index = WaitForMultipleObjects(
        MAX_OPERATIONS - completedCount,
        hEvents,    // 注意:需要调整等待的事件数组
        FALSE,      // 等待任意一个完成
        INFINITE
    );
    
    if (index >= WAIT_OBJECT_0 && index < WAIT_OBJECT_0 + MAX_OPERATIONS) {
        int operationIndex = index - WAIT_OBJECT_0;
        
        // 获取这个操作的完成状态
        DWORD bytesTransferred = 0;
        BOOL success = GetOverlappedResult(
            hFile,
            &overlappedArray[operationIndex],
            &bytesTransferred,
            FALSE
        );
        
        if (success) {
            printf("操作 %d 完成,传输了 %lu 字节\n", 
                   operationIndex, bytesTransferred);
            
            // 处理这个缓冲区的数据...
            // ProcessBuffer(buffers[operationIndex], bytesTransferred);
        }
        
        // 从等待列表中移除这个事件
        // 可以关闭事件句柄,或重新用于其他操作
        CloseHandle(hEvents[operationIndex]);
        hEvents[operationIndex] = INVALID_HANDLE_VALUE;
        
        completedCount++;
    }
}

事件重置的重要性

在使用手动重置事件时,需要特别注意重置时机。如果事件是手动重置的,在WaitForSingleObject返回后,事件仍然保持有信号状态。如果要复用这个事件进行另一次I/O操作,必须显式重置它。

c 复制代码
// 使用手动重置事件,可以多次等待同一个事件
HANDLE hManualEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
OVERLAPPED overlapped = {0};
overlapped.hEvent = hManualEvent;

// 第一次I/O操作
ReadFile(hFile, buffer1, 1024, NULL, &overlapped);

// 等待完成
WaitForSingleObject(hManualEvent, INFINITE);
GetOverlappedResult(hFile, &overlapped, &bytesRead1, FALSE);

// 重置事件以便重用
ResetEvent(hManualEvent);

// 使用同一个事件进行第二次操作
overlapped.Offset += 1024;  // 更新读取位置
ReadFile(hFile, buffer2, 1024, NULL, &overlapped);
WaitForSingleObject(hManualEvent, INFINITE);
GetOverlappedResult(hFile, &overlapped, &bytesRead2, FALSE);

对于自动重置事件,系统会在唤醒一个等待线程后自动重置事件状态。但要注意,如果多个线程在等待同一个自动重置事件,每次触发只能唤醒一个线程。

实际应用中的考量

在实际代码中,通过事件内核对象处理异步I/O需要考虑几个方面。超时处理很重要,特别是在网络I/O中。WaitForSingleObjectWaitForMultipleObjects都支持超时参数,但需要合理设置时间。

c 复制代码
DWORD waitResult = WaitForSingleObject(overlapped.hEvent, 5000); // 5秒超时
if (waitResult == WAIT_TIMEOUT) {
    // 超时处理
    CancelIo(hFile);  // 尝试取消未完成的I/O
    
    // 然后必须等待操作真正完成
    WaitForSingleObject(overlapped.hEvent, INFINITE);
    GetOverlappedResult(hFile, &overlapped, &bytesRead, FALSE);
}

资源管理也要注意。每个未完成的I/O操作都需要一个事件对象,大量并发操作会占用不少资源。有些设计会复用事件对象,但这样增加了复杂性。

这种方法在较新的Windows版本中已经不是首选方案。I/O完成端口通常能提供更好的性能,特别是对于高并发的服务器应用。但对于客户端应用或并发不高的场景,事件机制仍然简单有效。

与I/O完成端口的比较

通过事件内核对象接收I/O完成通知是较早期的异步I/O模型,它有几个特点值得注意。这种方法相对简单,容易理解,但扩展性有限。每个I/O操作都需要一个事件对象,大量并发操作时需要创建大量内核对象,这会消耗系统资源。

I/O完成端口采用不同模型。它使用一个队列来管理所有完成通知,工作线程池从队列中取出完成项并处理。这种方式减少了内核对象的数量,线程切换也更高效。但对于简单的应用程序,事件模型的简单性有时更有吸引力。

选择哪种方式取决于具体需求。如果只是偶尔进行异步I/O,或者并发量不大,事件模型完全够用。如果构建高并发的服务器应用,I/O完成端口通常是更好的选择。

相关推荐
阿冰冰呀10 小时前
互联网大厂Java求职面试实录:谢飞机的“水货”之路
java·mybatis·dubbo·springboot·线程池·多线程·hashmap
l1t11 小时前
DeepSeek辅助解决windows 11 wsl2中Linux版Dbeaver显示中文
linux·运维·windows
love530love13 小时前
Clink 调校指南:让 Windows CMD 拥有现代终端的便捷体验
人工智能·windows·python·cmd·clink
奋斗的小青年I17 小时前
Proxmox VE Ceph 超融合集群落地实战
windows·ceph·vmware·pve·超融合·proxmox
不做超级小白18 小时前
一行命令修复 WSL 中 VS Code 报 `Exec format error`
windows
CyrusCJA19 小时前
在Windows系统上将Redis注册为系统服务使其实现开机自启
数据库·windows·redis·缓存
星辰徐哥21 小时前
OpenCV入门:Windows系统下OpenCV的安装与环境配置
人工智能·windows·opencv
love530love1 天前
Clink 在 VS 2022 Developer Command Prompt 中的配置与路径精简调校
人工智能·windows·microsoft·clink
charlie1145141911 天前
通用GUI编程技术——图形渲染实战(三十六)——Constant Buffer与数据传递:CPU-GPU通信通道
开发语言·c++·windows·c·图形渲染·win32