一 基本概念
设备---windows操做系统上容许通讯的任何东西,好比文件、目录、串行口、并行口、邮件槽、命名管道、无名管道、套接字、控制台、逻辑磁盘、物理 磁盘等。绝大多数与设备打交道的函数都是CreateFile/ReadFile/WriteFile等。因此咱们不能看到**File函数就只想到文件 设备。html
与设备通讯有两种方式,同步方式和异步方式。同步方式下,当调用ReadFile函数时,函数会等待系统执行完所要求的工做,而后才返回;异步方式下,ReadFile这类函数会直接返回,系统本身去完成对设备的操做,而后以某种方式通知完成操做。ios
重叠I/O----顾名思义,当你调用了某个函数(好比ReadFile)就马上返回作本身的其余动做的时候,同时系统也在对I/0设备进行你要求的操 做,在这段时间内你的程序和系统的内部动做是重叠的,所以有更好的性能。因此,重叠I/O是用于异步方式下使用I/O设备的。算法
重叠I/O须要使用的一个很是重要的数据结构OVERLAPPED。编程
完成端口---是一种WINDOWS内核对象。完成端口用于异步方式的重叠I/0状况下,固然重叠I/O不必定非使用完成端口不可,还有设备内核对象、事 件对象、告警I/0等。可是完成端口内部提供了线程池的管理,能够避免反复建立线程的开销,同时能够根据CPU的个数灵活的决定线程个数,并且可让减小 线程调度的次数从而提升性能。windows
二 OVERLAPPED数据结构
typedef struct _OVERLAPPED
{
ULONG_PTR Internal;//被系统内部赋值,用来表示系统状态
ULONG_PTR InternalHigh;// 被系统内部赋值,传输的字节数
union {
struct
{
DWORD Offset;//和OffsetHigh合成一个64位的整数,用来表示从文件头部的多少字节开始
DWORD OffsetHigh;//操做,若是不是对文件I/O来操做,则必须设定为0
};
PVOID Pointer;
};
HANDLE hEvent;//若是不使用,就务必设为0,不然请赋一个有效的Event句柄
} OVERLAPPED, *LPOVERLAPPED;api
下面是异步方式使用ReadFile的一个例子
OVERLAPPED Overlapped;
Overlapped.Offset=345;
Overlapped.OffsetHigh=0;
Overlapped.hEvent=0;
//假定其余参数都已经被初始化
ReadFile(hFile,buffer,sizeof(buffer),&dwNumBytesRead,&Overlapped);
这样就完成了异步方式读文件的操做,而后ReadFile函数返回,由操做系统作本身的事情吧数组
下面介绍几个与OVERLAPPED结构相关的函数
等待重叠I/0操做完成的函数
BOOL GetOverlappedResult (
ANDLE hFile,
LPOVERLAPPED lpOverlapped,//接受返回的重叠I/0结构
LPDWORD lpcbTransfer,//成功传输了多少字节数
BOOL fWait //TRUE只有当操做完成才返回,FALSE直接返回,若是操做没有完成,经过调//用GetLastError ( )函数会返回ERROR_IO_INCOMPLETE
);缓存
宏HasOverlappedIoCompleted能够帮助咱们测试重叠I/0操做是否完成,该宏对OVERLAPPED结构的Internal成员进行了测试,查看是否等于STATUS_PENDING值。安全
三 完成端口的内部机制
建立完成端口
完成端口是一个内核对象,使用时他老是要和至少一个有效的设备句柄进行关联,完成端口是一个复杂的内核对象,建立它的函数是:
HANDLE CreateIoCompletionPort(
IN HANDLE FileHandle,
IN HANDLE ExistingCompletionPort,
IN ULONG_PTR CompletionKey,
IN DWORD NumberOfConcurrentThreads );
一般建立工做分两步:
第一步,建立一个新的完成端口内核对象,可使用下面的函数:
HANDLE CreateNewCompletionPort(DWORD dwNumberOfThreads)
{
return CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,dwNumberOfThreads);
};
第二步,将刚建立的完成端口和一个有效的设备句柄关联起来,可使用下面的函数:
bool AssicoateDeviceWithCompletionPort(HANDLE hCompPort,HANDLE hDevice,DWORD dwCompKey)
{
HANDLE h=CreateIoCompletionPort(hDevice,hCompPort,dwCompKey,0);
return h==hCompPort;
};
说明
1) CreateIoCompletionPort函数也能够一次性的既建立完成端口对象,又关联到一个有效的设备句柄
2) CompletionKey是一个能够本身定义的参数,咱们能够把一个结构的地址赋给它,而后在合适的时候取出来使用,最好要保证结构里面的内存不是分配在栈上,除非你有十分的把握内存会保留到你要使用的那一刻。
3) NumberOfConcurrentThreads一般用来指定要容许同时运行的的线程的最大个数。一般咱们指定为0,这样系统会根据CPU的个数来自动肯定。
建立和关联的动做完成后,系统会将完成端口关联的设备句柄、完成键做为一条纪录加入到这个完成端口的设备列表中。若是你有多个完成端口,就会有多个对应的设备列表。若是设备句柄被关闭,则表中自动删除该纪录。
完成端口线程的工做原理
完成端口能够帮助咱们管理线程池,可是线程池中的线程须要咱们使用_beginthreadex来建立,凭什么通知完成端口管理咱们的新线程呢?答案在函数GetQueuedCompletionStatus。该函数原型:
BOOL GetQueuedCompletionStatus(
IN HANDLE CompletionPort,
OUT LPDWORD lpNumberOfBytesTransferred,
OUT PULONG_PTR lpCompletionKey,
OUT LPOVERLAPPED *lpOverlapped,
IN DWORD dwMilliseconds
);
这个函数试图从指定的完成端口的I/0完成队列中抽取纪录。只有当重叠I/O动做完成的时候,完成队列中才有纪录。凡是调用这个函数的线程将被放入到完成端口的等待线程队列中,所以完成端口就能够在本身的线程池中帮助咱们维护这个线程。
完 成端口的I/0完成队列中存放了当重叠I/0完成的结果---- 一条纪录,该纪录拥有四个字段,前三项就对应GetQueuedCompletionStatus函数的二、三、4参数,最后一个字段是错误信息 dwError。咱们也能够经过调用PostQueudCompletionStatus模拟完成了一个重叠I/0操做。
当I/0完成队列中出现 了纪录,完成端口将会检查等待线程队列,该队列中的线程都是经过调用GetQueuedCompletionStatus函数使本身加入队列的。等待线程 队列很简单,只是保存了这些线程的ID。完成端口会按照后进先出的原则将一个线程队列的ID放入到释放线程列表中,同时该线程将从等待 GetQueuedCompletionStatus函数返回的睡眠状态中变为可调度状态等待CPU的调度。
基本上状况就是如此,因此咱们的线程要想成为完成端口管理的线程,就必需要调用
GetQueuedCompletionStatus函数。出于性能的优化,实际上完成端口还维护了一个暂停线程列表,具体细节能够参考《Windows高级编程指南》,咱们如今知道的知识,已经足够了。
线程间数据传递
线程间传递数据最经常使用的办法是在_beginthreadex函数中将参数传递给线程函数,或者使用全局变量。可是完成端口还有本身的传递数据的方法,答案就在于CompletionKey和OVERLAPPED参数。
CompletionKey 被保存在完成端口的设备表中,是和设备句柄一一对应的,咱们能够将与设备句柄相关的数据保存到CompletionKey中,或者将 CompletionKey表示为结构指针,这样就能够传递更加丰富的内容。这些内容只能在一开始关联完成端口和设备句柄的时候作,所以不能在之后动态改 变。
OVERLAPPED参数是在每次调用ReadFile这样的支持重叠I/0的函数时传递给完成端口的。咱们能够看到,若是咱们不是对文件设 备作操做,该结构的成员变量就对咱们几乎毫无做用。咱们须要附加信息,能够建立本身的结构,而后将OVERLAPPED结构变量做为咱们结构变量的第一个 成员,而后传递第一个成员变量的地址给ReadFile函数。由于类型匹配,固然能够经过编译。当GetQueuedCompletionStatus函 数返回时,咱们能够获取到第一个成员变量的地址,而后一个简单的强制转换,咱们就能够把它看成完整的自定义结构的指针使用,这样就能够传递不少附加的数据 了。太好了!只有一点要注意,若是跨线程传递,请注意将数据分配到堆上,而且接收端应该将数据用完后释放。咱们一般须要将ReadFile这样的异步函数 的所须要的缓冲区放到咱们自定义的结构中,这样当GetQueuedCompletionStatus被返回时,咱们的自定义结构的缓冲区变量中就存放了 I/0操做的数据。
CompletionKey和OVERLAPPED参数,均可以经过GetQueuedCompletionStatus函数得到。
线程的安全退出
不少线程为了避免止一次的执行异步数据处理,须要使用以下语句
while (true)
{
....
GetQueuedCompletionStatus(...);
}
那么如何退出呢,答案就在于上面曾提到的PostQueudCompletionStatus函数,咱们能够用它发送一个自定义的包含了OVERLAPPED成员变量的结构地址,里面包含一个状态变量,当状态变量为退出标志时,线程就执行清除动做而后退出。