Windows编程_Lesson004_项目预备_异步IO操作(使用IOCP实现大文件拷贝的项目)
2017-05-19 00:19
645 查看
异步IO机制
异步IO是Windows给我们读写文件提供的的一种的机制,在我们执行CreateFileEx函数是,通过传递相应的参数,就会向操作系统发送请求,那么CreateFileEx函数就会直接返回,它不会等到这个函数操作完成才返回,返回后,这个线程就可以做一些其它的操作,直到收到操作系统完成文件操作的通知,再去处理文件相关的操作,这样不会导致当前的线程发生阻塞;当操作系统收到这个请求时,就会进行实际的操作文件,当实际的操作完成后,它会通知执行CreateFileEx的线程,告诉线程可以进行文件操作了。异步操作-CreateFile
我们再来思考一个问题,同步IO为什么会导致程序阻塞?首先我们先说两个概念,进程和线程。
进程是指的是当前程序运行时所占用的空间,也就是说线程主要是来做存储的事情;
线程是实际的运行单元(工作),是与CPU直接打交道的。
所以当我们执行某些操作导致阻塞时,实际上指的是线程被阻塞了。
我们举一个不是很恰当的例子,进程就好比我们实际生活中的工厂,工厂本身是不能工作的,它只是说明占地面积是多少,拥有多少资源等等,而实际工作的是工厂里面的工人,一个工人就好比一个线程,此时就有同学再问,那工厂不一定只有一个工人吧,应该有多个工人吧?对了,此时我们在程序中就成为多线程。
多线程是一个进程中有多个线程在同时运行,我们称之为多线程。
那么想用实现异步IO操作时,我们可以使用使用创建线程来完成,我们也可以使用系统线程来完成。
OVERLAPPED结构体
OVERLAPPED结构体定义如下: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位的变量,
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提醒的例子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进行操作的例子
// 事件内核对象例子 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.判断请求是否完成 // 可提醒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进行操作的例子
// 完成端口 // 串行模型来京异步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完成端口实现高效的文件拷贝小项目
#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两个结构体变量赋值的时候出现的问题,不仔细说了,直接上代码吧,相信这一次会令您满意的。#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操作
- Windows编程_Lesson004_项目预备_重新认识MFC
- 【C语言】没事可以试试这个小程序,使用文件操作,模拟实现一个简单的文件拷贝工具!
- Java编程___File各类方法使用(实现拷贝特定文件至特定目录下)
- 使用pscp实现Windows 和 linux服务器间远程拷贝文件
- 使用Windows的SHFileOperation外壳函数实现文件操作
- Windows编程_Lesson005_项目预备_初识进程
- 使用pscp实现Windows 和 Linux服务器间远程拷贝文件
- IO流操作三 : 使用字节数组流实现文件的拷贝
- 使用pscp实现Windows 和 Linux服务器间远程拷贝文件
- Windows下使用scapy+python2.7实现对pcap文件的读写操作
- (10) Linux ----- 使用pscp实现Windows 和 Linux服务器间远程拷贝文件
- 使用pscp实现Windows 和 Linux服务器间远程拷贝文件
- 使用pscp实现Windows 和 Linux服务器间远程拷贝文件
- 项目开发技巧(四):使用JspSmartupload实现文件上传下载(一):JspSmart之upload组件源码及使用
- 使用VBA操作文件(3):Windows Scripting Host
- 使用Bash Shell对目录中的文件实现递归式拷贝
- Windows上使用VIM入门之文件操作
- C语言编程实现使用AES对文件进行加密
- 使用HDFS API实现hadoop HDFS文件系统的基本操作