【第19节】windows sdk编程:文件I/O
目录
引言
一、重叠I/O(异步I/O)
1.1使用等待信号的方式获得重叠I/O完成的通知
1.2使用异步调用的方式获得重叠I/O完成的通知
1.3 异步I/O实现方式总结
二、IOCP(完成端口)
2.1 关键函数:
2.2 IOCP的优势与原理
2.3 IOCP实现过程中的队列
2.4 APC和IOCP异同点
引言
在计算机编程领域,文件的输入输出(I/O)操作是一项基础且关键的任务。在多线程环境下进行文件I/O时,同步与效率问题尤为突出。传统的同步I/O方式可能导致线程阻塞,影响程序整体性能。为解决这些问题,Windows系统提供了诸如重叠I/O(异步I/O)、IOCP(完成端口)等高效机制。接下来,我们将深入探讨这些机制的原理、相关函数的使用方法,以及它们在实际应用中如何发挥作用。
一、重叠I/O(异步I/O)
I/O 其实就是“input/output”的意思,简单来讲就是输入输出,也可以理解为数据的流动,这个概念我们得先弄明白。
一般来说,当我们读取文件的时候,线程会被卡住,也就是线程要一直等着读取操作结束才行,这种方式就叫做同步 I/O。可能你觉得这挺正常的,还会想不就是要等文件读完才能处理信息嘛。但其实不是这样,举个例子,就像在厨房做饭,要是非得等水烧开了才去切菜,或者先切完菜才去烧水,虽然也能做,但效率就会变得很低。
Windows 系统在底层弄出了一种更高效的办法,叫重叠 I/O,很多资料里也把它叫做异步 I/O。异步 I/O 有个特点,当你调用读取文件的函数时,这个函数会马上给出返回,但实际上文件还没彻底读完,而是让系统底层自己去处理了,这样一来,文件读取的操作就不会让线程卡住了。不过这里有个麻烦事儿,就是程序要怎么知道文件已经读完了呢?这就跟烧开水一样,你去切菜了,切完菜发现水烧干了,肯定会觉得很麻烦。就像大家喜欢水开了会响的电水壶一样,有个提示才好。
Windows 可不傻,它准备了一些办法,能确保在 I/O 操作完成的时候告诉你,这可是异步 I/O 的关键机制,大家一定要记好咯。
在这之前,我们得先了解相关函数的用法:
1. 创建一个文件:使用`CreateFile`函数
HANDLE CreateFile(
_In_ LPCTSTR lpFileName, // 文件名
_In_ DWORD dwDesiredAccess, // 创建方式
_In_ DWORD dwShareMode // 共享方式
);
2. 读取文件:使用`ReadFile`函数
BOOL ReadFile(
_In_ HANDLE hFile, // 文件句柄
_Out_ LPVOID lpBuffer, // 缓冲区
_In_ DWORD nNumberOfBytesToRead, // 要读取的字节数
_Out_opt_ LPDWORD lpNumberOfBytesRead, // 实际读取的字节数
_Inout_opt_ LPOVERLAPPED lpOverlapped // 重叠结构
);
3. 写入文件:使用`WriteFile`函数
BOOL WriteFile(
_In_ HANDLE hFile, // 文件句柄
_In_ LPCVOID lpBuffer, // 缓冲区
_In_ DWORD nNumberOfBytesToWrite, // 要写入的字节数
_Out_opt_ LPDWORD lpNumberOfBytesWritten, // 实际写入的字节数
_Inout_opt_ LPOVERLAPPED lpOverlapped // 重叠结构
);
4. 重叠结构体:
typedef struct _OVERLAPPED {
ULONG_PTR Internal; // 保留没有用
ULONG_PTR InternalHigh; // 保留没有用
union {
struct {
DWORD Offset; // 要进行输入输出的位置
DWORD OffsetHigh; // 位置的高32位
} DUMMYSTRUCTNAME;
PVOID Pointer;
} DUMMYUNIONNAME;
HANDLE hEvent; // 进行通知的事件对象
} OVERLAPPED, *LPOVERLAPPED;
当使用这些函数并采用重叠I/O方式时,一旦句柄是以重叠I/O的方式打开的,会有两个特性:
1. 句柄变为可等待的对象,具有激发态和非激发态。
2. 文件指针失效,需要用`OVERLAPPED`结构体中的`Offset`表示读取或者写入的位置,不能再使用`SetFilePointer`这样的函数。
1.1使用等待信号的方式获得重叠I/O完成的通知
有两种等待信号的办法:
1. 直接等着句柄。
2. 等着`Overlapped`里的`hEvent`句柄。
具体这么操作:
第一步,用`HANDLE hFile = CreateFile(“XXX”, FILE_FLAG_OVERLAPPED, XXX, XXX);`来得到一个能用于异步I/O的文件句柄。接着就跟平常一样,用这个句柄做文件读取或者写入,比如`ReadFile(hFile, 缓冲区, 长度, pOverlapped);`,这个函数会马上返回结果,之后你就得找个地方等着接收通知。
直接等待句柄
最基础的重叠I/O操作,就是把文件句柄当作等待的目标。一旦I/O操作完成,句柄就会进入激发态。这时候用`WaitForSingleObject(hFile, -1);`来等待,然后再靠`GetOverlappedResult()`函数,就能判断重叠I/O是不是正常完成了。
等待事件对象
要是`OVERLAPPED`结构体里的`hEvent`设成`NULL`,I/O完成后,句柄会变成激发态。但要是创建一个事件对象,把它赋值给`hEvent`,那I/O完成的时候,变成激发态的就是这个事件对象了。
等待事件对象代码示例:
// 每一次文件读取/写入操作,都要创建一个线程,
// 系统同时运行的线程数是有限的,线程的创建和销毁浪费了大量资源
// 没有效率
#include "stdafx.h"
#include <windows.h>
typedef struct _MYOVERLAPPED {
OVERLAPPED ol;
HANDLE hFile;
PBYTE pBuf;
int nIndex;
}MYOVERLAPPED, *PMYOVERLAPPED;
DWORD WINAPI ThreadProc(LPVOID lParam) {
PMYOVERLAPPED pol = (PMYOVERLAPPED)lParam;
printf("开始等待......\n");
WaitForSingleObject(pol->ol.hEvent, INFINITE);
for (int i = 0; i < 10; i++)
{
printf("%d:%02x \n", pol->nIndex, pol->pBuf[i]);
}
printf("读完了!\n");
return 0;
}
int main()
{
// 1. 异步IO标记
// 有了这个标记 该文件就变为可等待的内核对象
// 后面的read write函数就变为非阻塞的
HANDLE hFile = CreateFile(L"..\\Debug\\123.exe", GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
// 2. 文件读取
PMYOVERLAPPED pol = new MYOVERLAPPED{};
pol->ol.Offset = 0x100;// 从偏移0x100这个位置开始读
pol->ol.hEvent = CreateEvent(NULL,NULL,FALSE,NULL); //系统读取完成后,会把我的hFile变为有信号状态
pol->hFile = hFile;// 被无视
pol->pBuf = new BYTE[0x1000]{};
pol->nIndex = 1;
ReadFile(hFile,
pol->pBuf,
0x1000,
NULL,//实际读取的个数,由OVERLAPPED结构体指定
(LPOVERLAPPED)pol);
HANDLE hThread = CreateThread(NULL, NULL, ThreadProc, pol, NULL, NULL);
PMYOVERLAPPED pol2 = new MYOVERLAPPED{};
pol2->ol.Offset = 0x200;// 从偏移0x100这个位置开始读
pol2->ol.hEvent = CreateEvent(NULL, NULL, FALSE, NULL); //系统读取完成后,会把我的hFile变为有信号状态
pol2->hFile = hFile;// 被无视
pol2->pBuf = new BYTE[0x1000]{};
pol2->nIndex = 2;
ReadFile(hFile,
pol2->pBuf,
0x1000,
NULL,//实际读取的个数,由OVERLAPPED结构体指定
(LPOVERLAPPED)pol2);
HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProc, pol2, NULL, NULL);
// ......干其他事
WaitForSingleObject(hThread, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
return 0;
}
等待文件对象代码示例:
#include "stdafx.h"
#include <windows.h>
typedef struct _MYOVERLAPPED{
OVERLAPPED ol;
HANDLE hFile;
PBYTE pBuf;
int nIndex;
}MYOVERLAPPED,*PMYOVERLAPPED;
DWORD WINAPI ThreadProc(LPVOID lParam) {
PMYOVERLAPPED pol = (PMYOVERLAPPED)lParam;
WaitForSingleObject(pol->hFile, INFINITE);
for (int i=0;i<10;i++)
{
printf("%d:%02x \n", pol->nIndex,pol->pBuf[i]);
}
printf("读完了!\n");
return 0;
}
int main()
{
// 1. 异步IO标记
// 有了这个标记 该文件就变为可等待的内核对象
// 后面的read write函数就变为非阻塞的
HANDLE hFile = CreateFile(L"..\\Debug\\123.exe", GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL| FILE_FLAG_OVERLAPPED, NULL);
// 2. 文件读取
PMYOVERLAPPED pol = new MYOVERLAPPED{};
pol->ol.Offset = 0x100;// 从偏移0x100这个位置开始读
// pol->hEvent == NULL; 系统读取完成后,会把我的hFile变为有信号状态
pol->hFile = hFile;
pol->pBuf = new BYTE[0x1000]{};
pol->nIndex =1;
ReadFile(hFile,
pol->pBuf,
0x1000,
NULL,//实际读取的个数,由OVERLAPPED结构体指定
(LPOVERLAPPED)pol);
HANDLE hThread = CreateThread(NULL, NULL, ThreadProc, pol, NULL, NULL);
PMYOVERLAPPED pol2 = new MYOVERLAPPED{};
pol2->ol.Offset = 0x200;// 从偏移0x100这个位置开始读
// pol->hEvent == NULL; 系统读取完成后,会把我的hFile变为有信号状态
pol2->hFile = hFile;
pol2->pBuf = new BYTE[0x1000]{};
pol2->nIndex = 2;
ReadFile(hFile,
pol2->pBuf,
0x1000,
NULL,//实际读取的个数,由OVERLAPPED结构体指定
(LPOVERLAPPED)pol2);
HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProc, pol2, NULL, NULL);
// ......干其他事
WaitForSingleObject(hThread, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
return 0;
}
1.2使用异步调用的方式获得重叠I/O完成的通知
前面说的等待信号的方法不太好用。因为用那些方法,得自己创建一个线程,专门去等着句柄或者事件对象变成激发态。而且要是有很多地方都在发起I/O操作,那就还得弄清楚到底是哪个句柄触发了I/O操作,很麻烦。
而异步过程调用给出了不一样的解决办法。当我们读取文件或者往文件里写入内容的时候,可以设置一个回调函数。这样一来,只要重叠I/O操作完成了,系统就会自动去调用这个回调函数。这种方式就叫做异步过程调用,也可以简称为APC调用。
异步过程调用需要使用读写文件的扩展函数`ReadFileEx`和`WriteFileEx`。使用APC调用,首先要知道回调函数原型:
typedef VOID (WINAPI *LPOVERLAPPED_COMPLETION_ROUTINE)(
_In_ DWORD dwErrorCode,
_In_ DWORD dwNumberOfBytesTransfered,
_Inout_ LPOVERLAPPED lpOverlapped
);
参数说明:
1. 参数1:`0`表示操作完成,其他错误码可查阅MSDN。
2. 参数2:I/O过程中传递的数据字节数。
3. 参数3:指向`OVERLAPPED`结构,是调用I/O函数时提供的结果体变量的地址。
异步过程调用函数:
BOOL ReadFileEx(
_In_ HANDLE hFile,
_Out_ LPVOID lpBuffer,
_In_ DWORD nNumberOfBytesToRead,
_Inout_ LPOVERLAPPED lpOverlapped
);
BOOL WriteFileEx(
_In_ HANDLE hFile,
_In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer,
_In_ DWORD nNumberOfBytesToWrite,
_Out_opt_ LPDWORD lpNumberOfBytesWritten,
_Inout_opt_ LPOVERLAPPED 1pOverlapped
);
要是用异步过程调用这种方式,`OVERLAPPED`里的事件对象,不管填了东西还是没填,都没什么影响,反正都会被系统忽略掉。而且,只要I/O操作完成了,系统就会自动去调用我们之前设置好的回调函数。
比较有意思的是,调用这个回调函数的线程,还是最开始发起重叠I/O操作的那个线程。这可能会让人觉得有点奇怪,毕竟这个线程本来就在做着自己的事儿呢。但实际上,系统会协调好它们之间的关系。一般来讲,当发起重叠I/O的线程处于可被唤醒的状态时,它就会去执行APC队列里的函数。
要是想让线程处于可警醒的状态,可以调用`WaitForSingleObjectEx`、`WaitForMultipleObjectEx`、`SleepEx`、`SignalObjectAndWait`、`MsgWaitForMultipleObjectsEx`这些函数。
异步过程调用(APC)代码示例:
#include "stdafx.h"
#include <windows.h>
typedef struct _MYOVERLAPPED {
OVERLAPPED ol;
HANDLE hFile;
PBYTE pBuf;
int nIndex;
}MYOVERLAPPED, *PMYOVERLAPPED;
// 提交任务的线程处理,其他线程看着
VOID CALLBACK FileIOCompletionRoutine(
_In_ DWORD dwErrorCode,
_In_ DWORD dwNumberOfBytesTransfered,
_Inout_ LPOVERLAPPED lpOverlapped
) {
PMYOVERLAPPED pol = (PMYOVERLAPPED)lpOverlapped;
for (int i = 0; i < 10; i++)
{
printf("%d:%02x \n", pol->nIndex, pol->pBuf[i]);
}
printf("读完了!\n");
}
int main()
{
// 1. 异步IO标记
// 有了这个标记 该文件就变为可等待的内核对象
// 后面的read write函数就变为非阻塞的
HANDLE hFile = CreateFile(L"..\\Debug\\123.exe", GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
// 2. 文件读取
PMYOVERLAPPED pol = new MYOVERLAPPED{};
pol->ol.Offset = 0x100;// 从偏移0x100这个位置开始读
// hEvent被无视 hFile也被无视
//pol->ol.hEvent = CreateEvent(NULL, NULL, FALSE, NULL); //系统读取完成后,会把我的hFile变为有信号状态
pol->hFile = hFile;// 被无视
pol->pBuf = new BYTE[0x1000]{};
pol->nIndex = 1;
ReadFileEx(hFile,
pol->pBuf,
0x1000,
(LPOVERLAPPED)pol,
FileIOCompletionRoutine);// 完成后直接调用该回调函数,不用等待文件句柄/事件对象
PMYOVERLAPPED pol2 = new MYOVERLAPPED{};
pol2->ol.Offset = 0x200;// 从偏移0x100这个位置开始读
//pol2->ol.hEvent = CreateEvent(NULL, NULL, FALSE, NULL); //系统读取完成后,会把我的hFile变为有信号状态
//pol2->hFile = hFile;// 被无视
pol2->pBuf = new BYTE[0x1000]{};
pol2->nIndex = 2;
ReadFileEx(hFile,
pol2->pBuf,
0x1000,
(LPOVERLAPPED)pol2,
FileIOCompletionRoutine);
// FileIOCompletionRoutine有系统调用
// 哪个线程执行该函数呢
// 哪个线程read/write 哪个线程执行
// ......干其他事
// 忙完了 想起来还有两个函数等着我呢
// CPU检测到当前线程的APC队列里有函数需要执行
// 就去执行该函数,执行完返回
// 只有当第2个参数是TRUE才去执行
SleepEx(200, TRUE);
// WaitForSingleObjectEx()
return 0;
}
1.3 异步I/O实现方式总结
相同点:
1. 三个程序都实现了异步IO操作,都使用了Windows的异步IO机制
2. 都使用了相同的`MYOVERLAPPED`结构体来管理异步IO操作
3. 都实现了从文件读取数据的功能
4. 都使用了`FILE_FLAG_OVERLAPPED`标志来创建可等待的文件句柄
5. 都实现了多段数据的并发读取
不同点:
1. 异步过程调用(APC)方式:
- 使用`ReadFileEx`函数而不是`ReadFile`
- 通过回调函数`FileIOCompletionRoutine`处理完成通知
- 不需要创建事件对象
- 使用`SleepEx`或`WaitForSingleObjectEx`来等待APC执行
- 回调函数在系统线程中执行
- 不需要创建额外的线程来处理完成通知
2. 等待事件对象方式:
- 使用`ReadFile`函数
- 为每个异步操作创建独立的事件对象
- 使用`WaitForSingleObject`等待事件对象
- 需要创建额外的线程来处理完成通知
- 每个异步操作都需要一个独立的事件对象和线程
3. 等待文件对象方式:
- 使用`ReadFile`函数
- 不创建事件对象,直接使用文件句柄作为同步对象
- 使用`WaitForSingleObject`等待文件句柄
- 需要创建额外的线程来处理完成通知
- 系统会自动将文件句柄设置为有信号状态
性能特点:
1. APC方式:
- 资源消耗最少,不需要额外的事件对象和线程
- 回调函数在系统线程中执行,可能会影响性能
- 适合简单的异步操作
2. 等待事件对象方式:
- 资源消耗最大,需要为每个操作创建事件对象和线程
- 控制最灵活,可以精确控制每个操作的完成通知
- 适合需要精确控制的复杂场景
3. 等待文件对象方式:
- 资源消耗适中,只需要创建线程
- 实现简单,不需要管理事件对象
- 适合中等复杂度的场景
这三种方式各有优劣,选择哪种方式主要取决于具体的应用场景和性能需求。APC方式最轻量级但灵活性较低,等待事件对象方式最灵活但资源消耗最大,等待文件对象方式则是一个比较好的折中方案。
二、IOCP(完成端口)
2.1 关键函数:
- 创建完成端口,绑定完成端口:使用`CreateIoCompletionPort`函数
HANDLE CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
- 获取完成通知:使用`GetQueuedCompletionStatus`函数
BOOL GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_Out_ LPDWORD lpNumberOfBytesTransferred,
_Out_ PULONG_PTR lpCompletionKey,
_Out_ LPOVERLAPPED* lpOverlapped,
_In_ DWORD dwMilliseconds
);
2.2 IOCP的优势与原理
如果要选重叠I/O最理想的通知方式,那非IOCP莫属,它是一种非常高效的异步I/O模型。IOCP涉及到后面要学习的线程池概念,而且这个线程池是我们自己实现的。
IOCP的基本运作方式是这样的:首先要创建一个带有最大线程数限制的完成端口,接着把用于重叠I/O操作的句柄和这个完成端口关联起来。从关联之后起,只要是跟这个句柄相关的重叠I/O操作完成了,相应的通知都会被发送到这个完成端口。这在表面上看,好像和事件对象没啥不同。
不过,IOCP真正厉害的地方在于,它能让多个线程同时等待这个完成端口。一旦有一个重叠I/O操作完成,就会唤醒其中一个等待的线程,并且把I/O操作的结果告诉这个被唤醒的线程。跟事件对象或者异步过程调用比起来,IOCP天生就支持多线程运作,而且它还能根据任务量的多少,自动调配线程去处理,不用我们手动操心。
2.3 IOCP实现过程中的队列
在整体实现IOCP的过程中,有5个队列:
- 设备列表:存储了与完成端口绑定的所有I/O设备。
- 完成队列:当I/O设备中有异步I/O操作完成,就会在完成队列中添加一项。
- 等待线程队列:存放等待的线程。
- 已释放线程列表:也就是正在运行中的线程列表。
- 已暂停线程列表:
当I/O设备把异步I/O操作做完后,会在完成队列里添加一条记录。这时候,系统就会去查看等待线程队列里有没有闲着的线程。要是有空闲线程,就会让`GetQueuedCompletionStatus`函数给出返回值,把那个空闲线程叫醒,然后把它加到已释放线程列表中。一般来说,已释放线程列表里的线程数量不会比创建完成端口时设定的最大线程数多。
不过,也有特殊情况。要是正在运行的线程调用了`sleep`函数,或者像`WaitForXXX`这类函数,进入了睡眠状态,那这个线程就会被挪到已暂停的线程列表里。这么做是为了不让处于暂停状态的线程占着运行中线程列表的位置。要是已释放线程列表已经满了,而这时候暂停的线程恢复了,那这个恢复的线程就会直接被放进已释放线程列表,这样一来,已释放线程列表里的线程数就可能会超过完成端口设定的最大线程数了。
至于到底创建多少个等待线程才合适,这没办法直接确定,得通过实际测试才能知道 。
IOCP代码示例:
#include "stdafx.h"
#include <windows.h>
typedef struct _MYOVERLAPPED {
OVERLAPPED ol;
HANDLE hFile;
PBYTE pBuf;
int nIndex;
}MYOVERLAPPED, *PMYOVERLAPPED;
DWORD WINAPI ThreadProc(LPVOID lParam) {
PMYOVERLAPPED pol =nullptr;
HANDLE hIOCP = lParam;
while (TRUE)
{
DWORD dwNum = 0;
ULONG_PTR uCompleteKey = 0;
BOOL bRet = GetQueuedCompletionStatus(
hIOCP,// 检查完成端口有没有待处理任务(有任务完成,通知完成端口处理)
&dwNum,// 实际传输的字节数
&uCompleteKey,// 哪个设备完成了异步操作
(LPOVERLAPPED*)&pol,
INFINITE);
if (bRet == FALSE && ERROR_ABANDONED_WAIT_0 == GetLastError())
{
printf("完成端口被关闭\n");
return 0;
}
if (uCompleteKey == NULL && pol == nullptr)
{
PostQueuedCompletionStatus(hIOCP, NULL, NULL, NULL);
printf("完成端口线程结束!\n");
return 0;
}
for (int i = 0; i < 10; i++)
{
printf("key:%d,%d:%02x \n", uCompleteKey,pol->nIndex, pol->pBuf[i]);
}
printf("读完了!\n");
}
return 0;
}
int main()
{
// 1. 异步IO标记
// 有了这个标记 该文件就变为可等待的内核对象
// 后面的read write函数就变为非阻塞的
HANDLE hFile = CreateFile(L"..\\Debug\\123.exe", GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
// 2. 创建一个完成端口
// 先获取核心数
SYSTEM_INFO si = {};
GetSystemInfo(&si);
HANDLE hIOCP = CreateIoCompletionPort(
INVALID_HANDLE_VALUE,// 创建完成端口,绑定完成端口的时候是文件句柄
NULL,// 创建完成端口的时候是NULL,绑定的时候是完成端口句柄
NULL,// 创建完成端口的时候是NULL,绑定的时候是自己设置的一个值,用来区分绑定的文件是哪个
si.dwNumberOfProcessors);// 完成端口上并行运行的最大线程数,绑定的时候是NULL
// 3.把完成端口和文件绑定,后面这个文件的所有异步操作的通知都交给完成端口
CreateIoCompletionPort(
hFile,
hIOCP,
0x12,// 和hFile绑定的值,也可以是hFile本身
NULL
);
// 4. 创建完成端口的服务线程
for (int i=0;i<si.dwNumberOfProcessors*2;i++)
{
CreateThread(NULL, NULL, ThreadProc, hIOCP, NULL, NULL);
}
// 5. 文件读取
PMYOVERLAPPED pol = new MYOVERLAPPED{};
pol->ol.Offset = 0x100;// 从偏移0x100这个位置开始读
pol->pBuf = new BYTE[0x1000]{};
pol->nIndex = 1;
ReadFile(hFile,
pol->pBuf,
0x1000,
NULL,//实际读取的个数,由OVERLAPPED结构体指定
(LPOVERLAPPED)pol);
PMYOVERLAPPED pol2 = new MYOVERLAPPED{};
pol2->ol.Offset = 0x200;// 从偏移0x100这个位置开始读
pol2->pBuf = new BYTE[0x1000]{};
pol2->nIndex = 2;
ReadFile(hFile,
pol2->pBuf,
0x1000,
NULL,//实际读取的个数,由OVERLAPPED结构体指定
(LPOVERLAPPED)pol2);
// ......干其他事
// 投递一个完成任务
PostQueuedCompletionStatus(hIOCP, NULL, NULL, NULL);
system("pause");
return 0;
}
2.4 APC和IOCP异同点
相同点:
1. 两个程序都实现了异步IO操作
2. 都使用了`MYOVERLAPPED`结构体来管理异步IO操作
3. 都实现了从文件读取数据的功能
4. 都使用了`FILE_FLAG_OVERLAPPED`标志来创建可等待的文件句柄
5. 都支持多段数据的并发读取
不同点:
1. 异步过程调用(APC)方式:
- 使用`ReadFileEx`函数进行异步读取
- 通过回调函数`FileIOCompletionRoutine`处理完成通知
- 不需要创建额外的线程池
- 使用`SleepEx`或`WaitForSingleObjectEx`来等待APC执行
- 回调函数在系统线程中执行
- 实现相对简单,代码量较少
- 适合简单的异步操作场景
2. 完成端口(IOCP)方式:
- 使用`ReadFile`函数进行异步读取
- 需要创建完成端口(IOCP)对象
- 需要创建线程池来处理完成通知
- 使用`GetQueuedCompletionStatus`等待完成通知
- 支持多个设备绑定到同一个完成端口
- 可以自定义完成键(Completion Key)来标识不同的设备
- 实现相对复杂,代码量较多
- 适合高并发、高性能的场景
性能特点:
1. APC方式:
- 资源消耗较少,不需要额外的线程池
- 回调函数在系统线程中执行,可能会影响性能
- 不适合高并发场景
- 实现简单,易于理解
2. IOCP方式:
- 资源消耗较大,需要维护线程池
- 性能最好,特别适合高并发场景
- 可以更好地利用系统资源
- 支持多个设备共享同一个完成端口
- 实现复杂,需要更多的代码管理
使用场景:
1. APC方式适合:
- 简单的异步IO操作
- 对性能要求不是特别高的场景
- 需要快速实现的场景
- 单设备异步操作
2. IOCP方式适合:
- 高并发服务器应用
- 需要处理大量并发IO操作的场景
- 对性能要求极高的场景
- 多设备并发操作的场景
总的来说,APC和IOCP是两种不同的异步IO实现方式,各有其适用场景。APC实现简单,适合简单的异步操作;而IOCP虽然实现复杂,但性能更好,特别适合高并发场景。选择哪种方式主要取决于具体的应用场景和性能需求。