我是靠谱客的博主 细腻微笑,这篇文章主要介绍Windows编程_Lesson004_项目预备_异步IO操作(使用IOCP实现大文件拷贝的项目),现在分享给大家,希望可以做个参考。

异步IO机制

异步IO是Windows给我们读写文件提供的的一种的机制,在我们执行CreateFileEx函数是,通过传递相应的参数,就会向操作系统发送请求,那么CreateFileEx函数就会直接返回,它不会等到这个函数操作完成才返回,返回后,这个线程就可以做一些其它的操作,直到收到操作系统完成文件操作的通知,再去处理文件相关的操作,这样不会导致当前的线程发生阻塞;当操作系统收到这个请求时,就会进行实际的操作文件,当实际的操作完成后,它会通知执行CreateFileEx的线程,告诉线程可以进行文件操作了。

这里写图片描述

异步操作-CreateFile

我们再来思考一个问题,同步IO为什么会导致程序阻塞?
首先我们先说两个概念,进程和线程。
进程是指的是当前程序运行时所占用的空间,也就是说线程主要是来做存储的事情;
线程是实际的运行单元(工作),是与CPU直接打交道的。
所以当我们执行某些操作导致阻塞时,实际上指的是线程被阻塞了。
我们举一个不是很恰当的例子,进程就好比我们实际生活中的工厂,工厂本身是不能工作的,它只是说明占地面积是多少,拥有多少资源等等,而实际工作的是工厂里面的工人,一个工人就好比一个线程,此时就有同学再问,那工厂不一定只有一个工人吧,应该有多个工人吧?对了,此时我们在程序中就成为多线程。
多线程是一个进程中有多个线程在同时运行,我们称之为多线程。
那么想用实现异步IO操作时,我们可以使用使用创建线程来完成,我们也可以使用系统线程来完成。

OVERLAPPED结构体

OVERLAPPED结构体定义如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _OVERLAPPED { ULONG_PTR Internal; ULONG_PTR InternalHigh; union { struct { DWORD Offset; DWORD OffsetHigh; } DUMMYSTRUCTNAME; PVOID Pointer; } DUMMYUNIONNAME; HANDLE hEvent; } OVERLAPPED, *LPOVERLAPPED;

我们先看下面的一个结构,它是用两个DWORD变量组成一个64位的变量,

复制代码
1
2
3
4
struct { DWORD Offset; DWORD OffsetHigh; } DUMMYSTRUCTNAME;

我们原来以同步IO方式打开一个对象时,这个对象保存了一个位置,我们可以通过函数来设置这个位置。但是以异步IO方式打开一个对象时, 这个对象里面并没有保存这个对象的位置,来开始进行访问,我们就需要设置这个结构体的值,来设置读取的位置。所以这个结构体设计的很巧妙,这个结构体可以帮我们完成文件分割的功能。
hEvent参数,它是一个事件内核对象,它会以事件的方式来通知我们的线程函数执行情况。实际上我们在实际的工作中,不仅仅只放一个hEvent内核对象,因为HANDLE就是一个void*指针,所以我们完全可以放一些其它对象的。
Internal参数主要是用来保存请求的错误码。
InternalHigh参数用来保存读取成功的字节数。

下面四种方法可以对异步I/O进行提醒
1. 设备内核对象
2. 事件内核对象(Windows中用途非常广泛的一种内核对象,它的作用主要用于同步以及交互,与设备是一种完全不同)
3. 可提醒I/O(不可跨线程)
4. I/O完成端口,

异步IO简单实例

1.设备内核对象进行I/O提醒的例子
这里写图片描述

复制代码
1
2
3
4
DWORD WINAPI WaitForSingleObject( _In_ HANDLE hHandle, _In_ DWORD dwMilliseconds );

Waits until the specified object is in the signaled state or the time-out interval elapses.
CSDN解释是这样的,翻译下来就是:
一直等待下去,直到指定对象达到信号态或者超过指定时间段。

2.事件内核对象对异步I/O进行操作的例子

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 事件内核对象例子 int main() { HANDLE hFile = CreateFile(TEXT("Demo.txt"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, nullptr); if (hFile != INVALID_HANDLE_VALUE) { // Read BYTE bReadBuffer[MAXBYTE] = { 0 }; OVERLAPPED oRead = { 0 }; oRead.Offset = 0; oRead.hEvent = CreateEvent(nullptr, TRUE, FALSE, TEXT("ReadEvent")); // 创建一个事件内核对象 ReadFile(hFile, bReadBuffer, sizeof(bReadBuffer), nullptr, &oRead); // Write BYTE bWriteBuffer[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; OVERLAPPED oWrite = { 0 }; oWrite.Offset = 0; oWrite.hEvent = CreateEvent(nullptr, TRUE, FALSE, TEXT("WriteEvent")); // 创建一个事件内核对象 ReadFile(hFile, bWriteBuffer, sizeof(bWriteBuffer), nullptr, &oWrite); // Do Something // 其它的线程 HANDLE hOverlapped[2] = { 0 }; hOverlapped[0] = oRead.hEvent; hOverlapped[1] = oWrite.hEvent; while (true) { DWORD dwCase = WaitForMultipleObjects(2, hOverlapped, FALSE, INFINITE); switch (dwCase-WAIT_OBJECT_0) { case 0: // 读完成 break; case 1: // 写完成 break; default: break; } } } else { GetLastError(); } return 0; }

3.可提醒I/O对异步I/O进行操作的例子

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 设备内核对象和事件内核对象相当于下面的过程 // 1.发送请求 // 2.做自己的事情 // 3.判断请求是否完成 // 可提醒I/O相当于下面的过程 // 1.发送请求 -> 完成后,操作系统提醒我 // 2.做自己的事情 // (可提醒I/O操作) // APC // 工厂(进程)->工人(线程) // 线程内部有 APC机制,当线程闲置时候(准确的说法是,当线程是可提醒状态时),这是前提,APC列表中的事情自动执行(即一系列的函数,它们会被挨个的被执行) // MessageBox -> 阻塞(闲置下来),但是它并不是可提醒状态 // Wait Sleep 这些函数才能真正使线程函数闲置下来,变为可提醒状态 VOID CALLBACK FileIOCompletionRoutine( _In_ DWORD dwErrorCode, _In_ DWORD dwNumberOfBytesTransfered, _Inout_ LPOVERLAPPED lpOverlapped ) { MessageBoxW(nullptr, TEXT("Read"), TEXT("Tips"), MB_OK); } // 可提醒I/O例子 int main() { HANDLE hFile = CreateFile(TEXT("Demo.txt"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, nullptr); if (hFile != INVALID_HANDLE_VALUE) { const UINT uLen = 255; BYTE bReadBuf[uLen] = { 0 }; OVERLAPPED oRead = { 0 }; oRead.Offset = 5; // 注意: 必须是ReadFileEx函数,才能设置线程为可提醒状态 ReadFileEx(hFile, bReadBuf, uLen, &oRead, FileIOCompletionRoutine); } // 只有设置为TRUE时候,APC函数才能被调用 // 如果使用Sleep函数,则不会弹出对话框 // 如果没有SleepEx,那么这个线程不是可提醒状态,所以不弹出对话框 SleepEx(100, TRUE); // 除了SleepEx函数外,还有其他的函数,也可以让线程处于可提醒状态,比如Wait等函数 // 可提醒I/O实际上是不好用的,因为回调函数里面的参数没有任何作用,因为我们不知道读到了什么值,只是知道多了多少个,因此没什么用。 // 所以不建议使用这种方式 // 但是APC的这套机制还是很好的(只不过不适合用在I/O上面),它能将我们的函数放到APC列表里面,我们可以理解APC是一个不定时的定时器,只要线程设置为可提醒状态,APC中的函数就能被执行。 return 0; }

运行效果如图所示:
这里写图片描述

4.I/O完成端口对异步I/O进行操作的例子
这里写图片描述

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 完成端口 // 串行模型来京异步I/O操作 // 并行模型 -> 多线程 // 1个工人 -> 5天 串行 // 5个工人 -> 1天 并行 // 单核 -> 模拟出来的多进程 线程 // 多喝 -> 多进程 核心数 -> #define IOCP_KEY_READ 1 // 完成端口例子 // 完成端口时Windows下一系列函数,是Windows给我们提供的一整套工具 // 天生就是一个并行模式 // 所以在Windows下进行异步I/O操作时,使用完成端口效率要高,但是这个也并不是绝对的,一定是在操作大文件时,效率才会提高,对于小文件,有可能效率还会降低 int main() { // 一个完成端口,会 // 创建设备队列 // 创建设备操作队列 // 创建线程池(多个线程) HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0); // 创建一个完成端口,第四个参数最重要,需要的线程数 // 此时传递的0,表示的是默认个数,比如一个核心,它会创建一个线程 // 就是有几个核心,就会创建几个线程 // 但是并不建议创建太大的线程数, HANDLE hFile = CreateFile(TEXT("Demo.txt"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, nullptr); //HANDLE hIOCP = CreateIoCompletionPort(hFile, nullptr, IOCP_KEY_READ, 0); // 这一行代码相当于前面的两行代码,创建并绑定 // 和设备绑定 CreateIoCompletionPort(hFile, hIOCP, IOCP_KEY_READ, 0); // 插入一个请求 PostQueuedCompletionStatus(hFile, ); // 该如何操作 GetQueuedCompletionStatus(hIOCP, ); // Windows CopyFile 使用完成端口来实现这个小项目 return 0; }

使用I/O完成端口实现高效的文件拷贝小项目

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#include <Windows.h> #include <iostream> #define IOCP_KEY_READ 1 #define IOCP_KEY_WRITE 2 int main() { LPCTSTR lpstrSrcFilePath = TEXT("Demo.txt"); LPCTSTR lpstrDestFilePath = TEXT("Demo-Clone.txt"); BOOL bOk = FALSE; BOOL bComplete = FALSE; do { // 1.打开一个设备(用来读) HANDLE hSrcFile = CreateFile(lpstrSrcFilePath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, nullptr); if (hSrcFile == INVALID_HANDLE_VALUE) break; // 2.打开一个设备(用来写) HANDLE hDestFile = CreateFile(lpstrDestFilePath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, hSrcFile); if (hDestFile == INVALID_HANDLE_VALUE) break; // 3.获取文件大小 LARGE_INTEGER liFileSize; if (!GetFileSizeEx(hSrcFile, &liFileSize)) break; // 4.设置文件指针 if (!SetFilePointerEx(hDestFile, liFileSize, nullptr, FILE_BEGIN)) break; // 5.设置文件末尾 if (!SetEndOfFile(hDestFile)) break; // 6.获取磁盘扇区大小 DWORD dwBytePerSector = 0; if (!GetDiskFreeSpace(TEXT("C:"), nullptr, &dwBytePerSector, nullptr, nullptr)) break; // 7.获取系统信息 SYSTEM_INFO sysInfo = { 0 }; GetSystemInfo(&sysInfo); // 8.创建I/O完成端口 HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, sysInfo.dwNumberOfProcessors); // 需要注意的是最后一个参数传递0,和传递 sysInfo.dwNumberOfProcessors 效果是一样的,这里就不多加说明了 if (hIOCP == NULL) { DWORD dwError = GetLastError(); if (dwError != ERROR_ALIAS_EXISTS) { // 此时才是真正的创建失败 break; } } // 9.将读和写的IOCP绑定到设备列表中 hIOCP = CreateIoCompletionPort(hSrcFile, hIOCP, IOCP_KEY_READ, sysInfo.dwNumberOfProcessors); hIOCP = CreateIoCompletionPort(hDestFile, hIOCP, IOCP_KEY_WRITE, sysInfo.dwNumberOfProcessors); OVERLAPPED oRead = { 0 }, oWrite = { 0 }; // 10.往IOCP完成队列里面发送一个写的项 // 否则GetQueuedCompletionStatus函数会一直阻塞的那里,因为此时并没有任意一件事情(读或写) PostQueuedCompletionStatus(hIOCP, 0, IOCP_KEY_WRITE, &oWrite); // 一般在另一个线程做,但是在这里,我们就用本线程来完成 DWORD dwByteTrans = 0; ULONG_PTR ulKey = 0; LPOVERLAPPED lpOverlapped = nullptr; // 11.分配空间,和new出来的空间一样 SIZE_T sizeLen = dwBytePerSector * 1024; LPVOID lpAddr = VirtualAlloc(nullptr, sizeLen, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); while (true) { BOOL bRet = GetQueuedCompletionStatus(hIOCP, &dwByteTrans, &ulKey, &lpOverlapped, INFINITE); if (bRet == FALSE) { if (lpOverlapped == NULL) { // 失败或者超时 break; } else { continue; } } switch (ulKey) { case IOCP_KEY_READ: { // 写操作 WriteFile // 主要对overlapped结构体进行更新,更新offset WriteFile(hDestFile, lpAddr, sizeLen, nullptr, &oWrite); LARGE_INTEGER liReadLen; liReadLen.QuadPart = dwByteTrans; if (oWrite.Offset + dwByteTrans == liFileSize.LowPart) { // 读写完成,程序退出 bComplete = TRUE; if (!SetEndOfFile(hDestFile)) break; } oRead.Offset += liReadLen.LowPart; oRead.OffsetHigh += liReadLen.HighPart; } break; case IOCP_KEY_WRITE: { // 更新offset LARGE_INTEGER liWriteLen; liWriteLen.QuadPart = dwByteTrans; oWrite.Offset += liWriteLen.LowPart; oWrite.OffsetHigh += liWriteLen.HighPart; // 判断当前文件长度 ReadFile(hSrcFile, lpAddr, sizeLen, nullptr, &oRead); } break; default: break; } if (bComplete) { // 实际上完成端口是不应该退出的,应该和程序的生命周期一样的 break; } } CloseHandle(hSrcFile); CloseHandle(hDestFile); bOk = TRUE; } while (false); if (!bOk) { DWORD dwError = GetLastError(); std::cout << "ErrorCode: " << dwError << std::endl; } return 0; }

上面文件拷贝小文件的改进

细心的朋友一定会发现,上面高效的文件拷贝小项目貌似并不是很完美,原因是当文件超过4GB的时候,它就会出现问题,为了解决这个问题,将部分代码做了修改,主要是在oRead和oWrite两个结构体变量赋值的时候出现的问题,不仔细说了,直接上代码吧,相信这一次会令您满意的。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#include <Windows.h> #include <iostream> #define IOCP_KEY_READ 1 #define IOCP_KEY_WRITE 2 int main() { LPCTSTR lpstrSrcFilePath = TEXT("Demo.iso"); LPCTSTR lpstrDestFilePath = TEXT("Demo-Clone.iso"); BOOL bOk = FALSE; BOOL bComplete = FALSE; do { // 1.打开一个设备(用来读) HANDLE hSrcFile = CreateFile(lpstrSrcFilePath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, nullptr); if (hSrcFile == INVALID_HANDLE_VALUE) break; // 2.打开一个设备(用来写) HANDLE hDestFile = CreateFile(lpstrDestFilePath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, hSrcFile); if (hDestFile == INVALID_HANDLE_VALUE) break; // 3.获取文件大小 LARGE_INTEGER liFileSize; if (!GetFileSizeEx(hSrcFile, &liFileSize)) break; // 4.设置文件指针 if (!SetFilePointerEx(hDestFile, liFileSize, nullptr, FILE_BEGIN)) break; // 5.设置文件末尾 if (!SetEndOfFile(hDestFile)) break; // 6.获取磁盘扇区大小 DWORD dwBytePerSector = 0; if (!GetDiskFreeSpace(TEXT("C:"), nullptr, &dwBytePerSector, nullptr, nullptr)) break; // 7.获取系统信息 SYSTEM_INFO sysInfo = { 0 }; GetSystemInfo(&sysInfo); // 8.创建I/O完成端口 HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, sysInfo.dwNumberOfProcessors); // 需要注意的是最后一个参数传递0,和传递 sysInfo.dwNumberOfProcessors 效果是一样的,这里就不多加说明了 if (hIOCP == NULL) { DWORD dwError = GetLastError(); if (dwError != ERROR_ALIAS_EXISTS) { // 此时才是真正的创建失败 break; } } // 9.将读和写的IOCP绑定到设备列表中 hIOCP = CreateIoCompletionPort(hSrcFile, hIOCP, IOCP_KEY_READ, sysInfo.dwNumberOfProcessors); hIOCP = CreateIoCompletionPort(hDestFile, hIOCP, IOCP_KEY_WRITE, sysInfo.dwNumberOfProcessors); OVERLAPPED oRead = { 0 }, oWrite = { 0 }; // 10.往IOCP完成队列里面发送一个写的项 // 否则GetQueuedCompletionStatus函数会一直阻塞的那里,因为此时并没有任意一件事情(读或写) PostQueuedCompletionStatus(hIOCP, 0, IOCP_KEY_WRITE, &oWrite); // 一般在另一个线程做,但是在这里,我们就用本线程来完成 DWORD dwByteTrans = 0; ULONG_PTR ulKey = 0; LPOVERLAPPED lpOverlapped = nullptr; // 11.分配空间,和new出来的空间一样 SIZE_T sizeLen = dwBytePerSector * 1024; LPVOID lpAddr = VirtualAlloc(nullptr, sizeLen, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); LARGE_INTEGER liReadLen = {0}, liWriteLen = {0}; while (true) { BOOL bRet = GetQueuedCompletionStatus(hIOCP, &dwByteTrans, &ulKey, &lpOverlapped, INFINITE); if (bRet == FALSE) { if (lpOverlapped == NULL) { // 失败或者超时 break; } else { continue; } } switch (ulKey) { case IOCP_KEY_READ: { // 写操作 WriteFile // 主要对overlapped结构体进行更新,更新offset WriteFile(hDestFile, lpAddr, sizeLen, nullptr, &oWrite); liReadLen.QuadPart += dwByteTrans; if (oWrite.Offset + dwByteTrans == liFileSize.LowPart && oWrite.OffsetHigh == liFileSize.HighPart) { // 读写完成,程序退出 bComplete = TRUE; if (!SetEndOfFile(hDestFile)) break; } oRead.Offset = liReadLen.LowPart; oRead.OffsetHigh = liReadLen.HighPart; } break; case IOCP_KEY_WRITE: { // 更新offset liWriteLen.QuadPart += dwByteTrans; oWrite.Offset = liWriteLen.LowPart; oWrite.OffsetHigh = liWriteLen.HighPart; // 判断当前文件长度 ReadFile(hSrcFile, lpAddr, sizeLen, nullptr, &oRead); } break; default: break; } if (bComplete) { // 实际上完成端口是不应该退出的,应该和程序的生命周期一样的 break; } } CloseHandle(hSrcFile); CloseHandle(hDestFile); bOk = TRUE; } while (false); if (!bOk) { DWORD dwError = GetLastError(); std::cout << "ErrorCode: " << dwError << std::endl; } return 0; }

最后

以上就是细腻微笑最近收集整理的关于Windows编程_Lesson004_项目预备_异步IO操作(使用IOCP实现大文件拷贝的项目)的全部内容,更多相关Windows编程_Lesson004_项目预备_异步IO操作(使用IOCP实现大文件拷贝内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(50)

评论列表共有 0 条评论

立即
投稿
返回
顶部