I/O完成端口是个什么鬼

I/O完成端口解决什么问题?

大的应用背景是异步I/O。什么是异步I/O呢?先说说同步I/O吧。一个线程在读取或者写入文件的时候,如果I/O没有完成,线程就要一直等待,干不了其他事情,这就是同步I/O。对于聪明而急性子的计算机科学家来说,这种读写方式也太low了,根本不能带来碾压众人智商的快感。于是异步I/O诞生了。它允许线程发出I/O请求后还能做其他事情。但是又带来了新的问题,I/O请求完成后如何通知线程处理。

 

线程有四种方式接收I/O请求完成通知 

一、触发设备内核对象方式

线程向I/O设备发出I/O请求的同时,将设备置为未触发状态,I/O完成后将自己设置为触发状态。线程将设备置为未触发状态后就一直盯着设备状态,发现是触发后知道I/O完成了。

这个方式还是很low,相当于线程将设备的一个小旗子降下来,然后盯着设备再升起小旗子。线程盯着设备小旗子的时候实际也是啥都没做,所以这种方式基本没用。

 

二、触发事件内核对象

原理和第一个差不多,只不过不再盯着I/O设备的小旗子,而是为每个I/O请求专门创建一个事件内核对象,靠着摆弄它的小旗子协调异步I/O。

它的缺点在于每次I/O都要创建一个事件内核对象,你说啰嗦不罗嗦。

 

三、可提醒I/O

终于来了个有点技术含量的。

首先你要记住,每个线程天生自带一个异步过程调用(APC)队列,本质上是一个回调函数队列。

可提醒I/O的工作过程是这样的。发出I/O请求的线程需要自定义一个回调函数,然后发出I/O请求以后它可以先去做别的事情。在设备处理完I/O请求以后,会在发出I/O请求的线程的APC队列中添加一项,包含了回调函数和一些相关参数。再说刚才的线程,它忙别的事情总要达到一个点,这时它必须对I/O结果进行处理。

这里就有几个问题需要讨论:第一,线程如何知道I/O完成了;第二,线程去哪里处理I/O完成项。线程可以调用6个可以将线程置为可提醒状态的函数。调用完以后,操作系统检查发现它的APC队列有未处理项,这种情况下线程就处于可提醒状态(而如果调用完那6个函数后,线程的APC队列为空,线程将进入睡眠状态),意思是操作系统会提醒线程去处理它的APC队列,这就实现了第一点。线程执行那个回调函数,处理I/O完成项,就实现了第二点。

简单的说,线程发出I/O请求后,还要自定义一个回调函数,这个回调函数在I/O设备完成I/O请求后被I/O设备添加到线程的APC队列,等待最后由线程自己完成处理。

可提醒I/O的缺点有两个:

1.线程必须创建一个回调函数处理I/O完成项,增加了代码的复杂性。使用回调函数也会增加使用全局变量的可能。

2.线程如果发出大量的I/O请求,过段时间它还要处理完成通知,导致其他线程长时间处于空闲状态,这样程序的伸缩性不好。

可提醒I/O基础设施的一个好处:可以进行线程间通信,甚至能够跨越进程的界限,但是只能传递一个值。实现方法是调用QueueUserAPC()函数后,会向线程的APC队列添加一项内容,这个内容包含了回调函数和必要的参数。这允许我们让目标线程执行传递到APC的回调函数,就是刚才说的线程通信。

 

四、I/O完成端口

虽然叫端口,但其实是一个内核对象,不是通信用的端口。I/O完成端口的出现,使得发出I/O请求的线程和处理I/O完成项的线程可以是不同的线程。请记住要想玩转I/O完成端口,需要这些角色密切配合:


1)发出I/0请求的线程

2)设备对象,线程通过设备驱动程序来读和写它们(I/O)

3)I/O完成端口

4)处理I/O完成项的线程池


首先,需要先创建一个I/O完成端口内核对象,并把要访问的设备的句柄关联到这个完成端口,这个两步可以通过一个函数实现:CreateIoCompletionPort()。注意,一个完成端口可以关联多个不同类型的设备。然后你需要创建一个线程池,用来处理I/O完成项。对于线程池里面的每一个线程,调用GetQueuedCompletionStatus()是告诉操作系统,我这个线程要处理某个I/O完成端口的完成项,这一步实现了完成端口和处理完成项线程的关联。该说发出I/O请求的线程了,其实就比较简单了,一旦有对设备的I/O访问,因为设备已经关联了I/O完成端口,那么这个I/O请求完成后就会添加到完成端口的I/O完成队列里面,等待线程池中的线程进行处理。这一段的流程是困扰了很久才梳理清楚的,这个总体的流程清楚后再去研究I/O完成端口的实现细节相信会轻松不少。

本质上,I/O完成端口将发出I/O的线程和处理I/O完成项的分开了,相对于可提醒I/O前进了一步。不仅如此,因为这种分开,使得每个发出I/O请求的线程的完成项都能得到处理,就不会存在可提醒I/O那种如果一个线程发出大量I/O请求,其他线程将长时间处于空闲的情况了。

还有一个重要特点是,I/O完成端口还能够设置允许多少线程(这里是线程池里的线程)同时处理I/O完成队列。通常来说,这个数值要小于线程池的数量。也就是说,线程池里存在等待状态的线程,这是为什么呢?这样设计的原因是为了最大程序的保证I/O完成队列能够及时得到处理。你想想,处理I/O完成项(从I/O完成队列获取)的线程保不准会将自己挂起的,例如wait一个内核对象,这时线程池的等待线程就能马上补上,确保对I/O完成队列的处理不会变慢。

另外,类似于可提醒I/O的QueueUserAPC()函数,windows还提供了PostQueuedCompletionStatus()函数。它允许调用者向I/O完成队列添加一项,想想这可以干什么,调用者可以和线程池里的线程通信。

 

好了,就说这些吧,纸上得来终觉浅,绝知此事要躬行。


这个我的微信公众号zhixin991,不关注下?

I/O完成端口是个什么鬼_第1张图片

你可能感兴趣的:(C++基础,windows编程)