异步IO机制
1:如果我们写的程序需要打开一个非常大的文件,我们使用的打开方式会打开很长时间才可以,可能需要数秒钟,文件越大,打开时间越长,在打开一些设备的时候,也可能发生阻塞,导致程序卡在这里,还可能设备无法打开,但是会耗费很多时间,这样的情况就需要用异步IO来解决。
2:异步IO,在调用CreateFile的时候,会向操作系统的设备
发送一个请求,此时CreateFile这个函数会直接返回,不会等待,发送请求后,操作系统会有实际的操作,他会设置一些通知,在这期间,程序可以做一些其他的事,等通知来了之后,IO操作就完成了。这样的模式可以使程序无需等待文件操作的完成。
异步操作——CreateFile:
1:进程就是当前程序运行起来的一块空间,而线程是实际的运行单位。
2:进程主要是用来做存储的事情,县城主要是来工作的,与CPU实际打交道的就是线程,当我们的某些操作停止或者阻塞的时候都是说我们当前的线程被停止或者阻塞。
3:进程相当于一座工厂,线程相当于工厂里面的设备,工人等。线程可以有多个,但是进程只能是一个。
4:要实现异步IO,只需要在CreateFile的时候将dwFlagAndAttributes参数设置成FILE_FLAG_OVERLAPPED,之后的所有操作,都会以异步的方式操作,包括ReadFile和WriteFile。异步的时候,ReadFile的最后一个参数就有意义了。
_OVERLAPPED结构体:
typedef struct _OVERLAPPED {
ULONG_PTR Internal; //操作系统保留,指出一个和系统相关的状态,请求的错去码
ULONG_PTR InternalHigh; //指出发送或接收的数据长度,保存传输成功的字节数。同步时,函数返回之后,会有dwReadSize也是这个字节数。
union {
struct {
DWORD Offset; //文件传送的字节偏移量的低位字
DWORD OffsetHigh; //文件传送的字节偏移量的高位字
};
PVOID Pointer; //指针,指向文件传送位置
};
HANDLE hEvent; //指定一个I/O操作完成后触发的事件,事件内核对象
} OVERLAPPED, *LPOVERLAPPED;
当指定以异步IO方式来打开一个文件的时候,我们可以使用函数来设置读取位置,但是以异步打开,就不能设置,只能通过参数的方式来告诉,使用这个结构体指定从哪里开始。这个操作非常巧妙,可以提高文件操作的效率,这个里面可以做一些文件分割的事。
异步IO实例:
1:
_In_ DWORD dwDesiredAccess = GENERIC_READ | GENERIC_WRITE,
_In_ DWORD dwShareMode = FILE_SHARE_READ,
_In_ DWORD dwCreationDisposition = OPEN_ALWAYS,
_In_ DWORD dwFlagsAndAttributes = FILE_FLAG_OVERLAPPED,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes = nullptr,
_In_opt_ HANDLE hTemplateFile = nullptr
HANDLE m_hFile_ = CreateFile(TEXT("111.txt"), dwDesiredAccess, dwShareMode, lpSecurityAttributes,
dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
OVERLAPPED overlapped = { 0 };
overlapped.Offset = 100;
BYTE bBuffer[MAXBYTE] = { 0 };
DWORD dwReadSize = 0;
BOOL bRet = ReadFile(m_hFile, bBuffer, MAXBYTE, nullptr, &overlapped);//读取文件
DWOED dwError = GetLastError();
if(bRet && dwError == ERROR_IO_PENDING)
{
//当前请求发送成功,否则认为请求失败。
WaitForSingleObject(m_hFile, INFINITE);//阻塞函数,阻塞到这个请求完成。
}
2:这样的的逻辑常用于,有一个用户请求,非常重要,必须马上响应,就可以使用这样的,可以先把用户的响应了,再用WaitForSingleObject(m_hFile, INFINITE)看是否执行成功。
使用事件内核对象提醒:
1:再使用异步I/O的时候,有四种提醒方式:
1:使用设备内核对象(就是上面那一种WaitForSingleObject())
2:使用事件内核对象(Windows内核里面用途非常广泛的内核对象)
3:可提醒I/O(不可跨线程)
4:I/O完成端口
2:所有的内核对象都是属于系统的,而程序是单独的,所以程序与程序之间的通讯可以使用内核对象来实现,使用SendMessage等也可以通讯。
3:
#include <windows.h>
int main()
{
HANDLE hFile = CreateFile(TEXT("Demo.txt"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, nullptr);
if (INVALID_HANDLE_VALUE != hFile)
{
//Read
BYTE bReadBuffer[100] = { 0 };
OVERLAPPED oRead = { 0 };
oRead.Offset = 0;
oRead.hEvent = CreateEvent(nullptr, TRUE, FALSE, TEXT("ReadEvent"));//创建事件内核对象,事件内核对象经常用于一些事件的触发。
//参数 1:安全指针,null代表该句柄不可被子进程继承,2:是否为人工复位,3:事件对象初始状态是否标记,4:名字
//一个内核对象在操作系统中是有一份,这个事件内核对象可用于本机多个程序之间的调试,多个程序共用最重要的指标就是name。
ReadFile(hFile, bReadBuffer, sizeof(bReadBuffer), nullptr, &oRead);
//第三个参数获取读到多少,在执行完这个函数后,异步,可能读取操作并没有完成,这个参数不能获取到读取了多少个
//Write
BYTE bWriteBuffer[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
OVERLAPPED oWrite = { 0 };
oWrite.Offset = 0;
oWrite.hEvent = CreateEvent(nullptr, TRUE, FALSE, TEXT("WriteEvent"));
WriteFile(hFile, bWriteBuffer, sizeof(bWriteBuffer), nullptr, &oWrite);
//Do Something
//其他线程
HANDLE hOverLapped[2] = { 0 };
hOverLapped[0] = oRead.hEvent;
hOverLapped[2] = oWrite.hEvent;
while (TRUE)
{
DWORD dwCase = WaitForMultipleObjects(2, hOverLapped, FALSE, INFINITE);//等待多个完成
switch (dwCase - WAIT_OBJECT_0)
{
case 0:
//读完成
break;
case 1:
//写完成;
break;
}
}
}
else
{
//GetLastError();
}
return 0;
}
:在ReadFile等完成的时候,会重置oRead等结构体里面的hEvent,我们就可以使用WaitForSingleObject等类型的函数来等待之间完成,并判断完成的什么事情。
可提醒IO:
1:上面用到的异步操作实际就是分为三步:1:发送请求,2:做自己的事情,3:判断请求是否完成。这样可以达到异步的效果,但是如果完成之后,操作系统来提醒我们的话,效率就会更高,这样,我们发送请求,完成之后操作系统就会通知我们是否完成。
2:APC:Windows里面的一种机制,线程内部有一个APC机制,就是指当线程空闲的时候来做APC列表中的事情,实际为当线程在可提醒状态下(空闲状态),APC列表里面的事项回本调用。实际APC列表里面就是一些函数。
3:线程的空闲并不是我们日常理解的那种,而是:例如:调用MessageBox的时候,会阻塞,等待我们点击确定取消等关闭窗口,在Windows里面,有Wait类型的函数,Sleep等函数可以使线程真正空闲下来,变成可提醒状态,当发送IO请求之后,他会生成一个函数在APC列表当中,当我们的线程变成可提醒的时候,这个函数就会被调用。
4:
#include <windows.h>
VOID CALLBACK FileIOCompletionReadRoutine(
DWORD dwErrorCode,
DWORD dwNumberOfBytesTransfered,
LPOVERLAPPED lpOverlapped)
{
MessageBox(nullptr, L"完成", L"提示", MB_OK);
}
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(hFile, &bReadBuf, uLen, &oRead, FileIOCompletionReadRoutine);
}
//下面,线程必须要处于可提醒状态。
SleepEx(10,TRUE);//第二参数决定sleep的时候是否可以为可运行状态。Sleep函数就是调用的SleepEx,并且第二参数为False。
return 0;
}
5:上面这种可提醒IO确实可以在执行完成之后并且线程处于空闲的时候,去提示执行完成,但是,在回调函数里面,我们并不能做有意义的事情(处理读取到的数据),除非将读取到的数据保存为全局变量里面,这样的话又会使我们的程序变得很乱,因此,上面这样的可提醒IO使用并不多。
完成端口:
概述:
1:这是目前使用异步I/O最为方便和科学的方式,前面面三种(设备内核对象,事件内核对象,可提醒I/O)都是在一个线程里面来做的,都是使用的串行模型来进行的异步I/O操作的。异步I/O最适合的是并行模型。
2:串行:一个线程,从上而下地做。
3:并行:有多线程,多个工人同时来做。
4:以前电脑CPU为单核的时候,计算机是模拟出来的多进程,多核CPU可实现真正的多线程(根据核心数决定能够运行多少条线程)。
5:多进程下还有多线程,两个进程之间的切换会耗费很多资源,在进程之下划分更多的小单元(线程),使得切换效率更高。单核的模拟多线程,如果做得不好,就可能导致其效率非常低。多核CPU出现之后,并行编程就变得非常广泛(一个核心在两个线程之间切换是非常快的,因此,常说四核八线程)。
6:完成端口机制的使用也只是在大文件的操作中会体现出来优势,在小文件中,还是可以使用以前的同步I/O方式来操作。
IOCP初窥:
1:完成端口是Windows给我们提供的一套工具库,一个完成端口会创建一个队列,包括:设备(可以使多个设备与一个完成端口对应)、设备操作队列、线程池(多个线程)。
2:CreateIoCompletionPort(),有两个功能:1:将设备与完成端口相绑定,2:可以创建一个完成端口。
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle, //有效的文件句柄或者INVALID_HANDLE_VALUE
_In_opt_ HANDLE ExistingCompletionPort, //传递已经存在的一个完成端口,传递NULL会新建一个IOCP
_In_ ULONG_PTR CompletionKey, //传递给处理函数的参数
_In_ DWORD NumberOfConcurrentThreads //是有多少个线程在访问这个消息队列。
//当参数ExistingCompletionPort不为0的时候,系统忽略该参数,当该参数为0表示允许同时相等数目于处理器个数的线程访问该消息队列。
);//当第四个参数超过物理线程数的时候,CPU就会进行线程上下文的切换,会耗费大量的资源。
3:无论给多少线程数,他实际最多只会有物理线程数个数的线程数运行,他会进行大量的线程上下文的切换。
4:完成端口的创建,失败返回NULL而非INVALID_HANDLE_VALUE,如果已存在这个名字的完成端口,创建会失败,GetLastError会返回ERROR_ALIAS_EXISTS。
总结:
1:设备内核对象方式:
在异步方式打开文件后,Read和Write后,就调用WaitForSingleObject();等待完成,在调用之前可以先做一些其他无关的事情。
2:事件内核对象方式:
在异步方式打开文件后,对
Read和Write的OVERLAPPED结构体的hEvent创建事件(CreateEvent)
,然后可在其他线程中等待完成(WaitForMultipleObjects),可同时检测多个事件的发生。
3:可提醒方式I/O:
在异步方式打开文件后,Read和Write需要调用Ex版本,指定其事件完成的回调函数,他会放在当前线程的APC列表中,当线程处于空闲状态(空闲且可运行)的时候,就会自动调用APC列表里面的函数(包括这个回调函数)处理事情。
4:完成端口:
创建完成端口,并绑定与之对应的对象(可以为文件内核对象),当完成端口里面的事件完成之后就会将之送到完成队列端口,使用GetQueuedCompletionStatus可获取完成状态,并作相应处理。
完成端口是一个异步IO模型,每一次完成之后有提醒(每一次事件完成之后,他会将完成的对象放在完成队列的端口里面去)。
Post的这个信息是一个已经完成的消息(已经放在完成队列端口的消息),完成端口是异步的,他每一步都有几部分,在不同线程中完成。
这就是异步IO的意义,每一个操作使用post,get到消息,看其完成状态是怎样的,根据完成状态判断下一步做什么事情。
Post的这个信息是一个已经完成的消息(已经放在完成队列端口的消息),完成端口是异步的,他每一步都有几部分,在不同线程中完成。
这就是异步IO的意义,每一个操作使用post,get到消息,看其完成状态是怎样的,根据完成状态判断下一步做什么事情。