技术开发 频道

.NET异步编程:IO完成端口与BeginRead

  除此之外,我们还要为该端口创建一些供使用的线程。然后让这些线程调用Windows提供的GetQueuedCompletionStatus方法。这些线程调用了该方法后会被放到IO完成端口另外一个数据结构中:一个后进先出的队列(我们将其称为等待队列吧)。然后该线程会休眠起来,不占用CPU。然后我们可以调用像ReadFile这样的方法发起一个IO请求:

BOOL ReadFile(
    HANDLE hFile,
    PVOID  pvBuffer,
    DWORD  nNumBytesToRead,
    PDWORD pdwNumBytes,
    OVERLAPPED
* pOverlapped);

ReadFile(..
&overlapped);

  上面代码中的OVERLAPPED是一个非常重要的数据结构,后面会提到。

  现在假设你的某个IO设备收到了一个数据包,Windows就会检查这个IO设备是否跟一个IO完成端口关联了,如果关联了Windows就会把这个数据包投递到这个IO完成端口。IO完成端口里还有另外一个先进先出的队列,用来保存这些IO完成的数据。IO完成端口一看,唔,有个IO完成包投递到我这儿来了,那我看看我的那个等待队列里有没有线程还在休息,如果有就叫它起来干活儿。嘿,还真有一个家伙还在睡觉,如是IO完成端口就唤醒该线程,实际上就是上面的那个GetQueuedCompletionStatus方法返回了。该方法返回时还会得到一些别的信息:接收了多少个字节啊,是哪个设备啊,最重要的是上面提到的OVERLAPPED这个结构等等。起来后的线程就会拿着这些信息干一些后续的事儿:

BOOL GetQueuedCompletionStatus(
    HANDLE     hCompletionPort,
    PDWORD     pdwNumberOfBytesTransferred,
    PULONG_PTR pCompletionKey,
    OVERLAPPED
**ppOverlapped,
    DWORD      dwMilliseconds);
//类似于下面的过程
//创建一个线程
Thread thread = new Thread(()=>{
    
while(true){
        
//如果没有IO完成通知到达,该线程就在这里休眠了
        if(GetQueuedCompletionStatus(hIoPort,..ppOverlapped..)){
            
//从ppOverlapped里取出所需的信息,比如可能设置了一个回调函数的指针等
        }else{
            
//
        }
    }
});
thread.Start();

  干完这个事儿后,这个线程又会回到刚才那个队列继续躺起来(其实是再次调用一下那个方法)。我们要注意的是,这个等待队列是后进先出的,也就是说如果下次有消息来了很有可能还是上会那个线程来处理。这样做的目的还是为了提高性能:不需要进行线程上下文切换。因为CPU的速度比IO设备的高出很多,大部分时候我们只需要一两个线程就可以处理很多IO请求。

  现在假设我们的机器有2个CPU,创建IO完成端口时我们指定了同时可以有2个线程运行。我们创建了4个线程放到等待队列里。现在有4个IO完成包投递过来了,放在那个队列里。实际上IO完成端口只会唤醒两个线程去执行,因为你指定了同时只能有两个线程运行,那两个线程运行完就会立马回来继续运行别的。但是现在出了一个状况,其中有一个线程执行过程中因为等待某个资源被阻塞了。那现在只有一个线程执行了,那这个线程就有点吃力了。其实IO完成端口非常聪明,它内部还有一个暂停运行的线程列表和一个正在运行的线程列表。如果某个线程正在运行,它就把这个线程ID放到这个队列里,当这个线程因为某个事儿暂停运行了它就会将其移动到另外一个列表中。IO完成端口会保证正在运行的线程列表里的数目不会超过你指定的最大并发数。一旦这个列表里的数目少于这个数,而IO完成包队列里又有未处理的包,IO完成端口就会看看还有没有在睡觉的线程,如果有就将其唤醒干活儿。

  IO完成端口尽量的控制同时运行的线程数,减少上下文切换浪费的时间和资源,并且让线程尽量的忙起来。

  这里还有一个有意思的地方,假设现在正在运行的两个线程其中一个调用Thread.Sleep休眠了,然后IO完成端口唤醒另外一个线程,让同时运行的线程数保持为2个,不过过了一会儿刚才调用Sleep休眠的线程醒过来了,有意思的事情发生了:现在有三个线程同时运行,超过了我们设置的最大并行数。这个时候IO完成端口是不会杀掉一个线程的,它会让它们继续执行,然后等到执行完了再让这个并行数降下去。

  实际上,IO完成端口不仅仅可以用来处理这种异步的IO,它完全可以作为一种线程间的通讯机制来使用(与IO一点关系都没有),我们可以调用Win API PostQueuedCompletionStatus来模拟一次IO完成,这样我们的IO完成端口就会接到通知,然后调用线程执行。熟悉并发里的Actor模型的同学可能觉得这有点Actor的影子了。

1
相关文章