win32 多线程知识点梳理六 IOCP

IOCP的全称就是I/O Completion ports。
虽然名称看上去相似,但是它和APCs 中所用的I/O completion routines 没有任何关联。
IOCP 可以解决目前为止我们看到的所有问题:

  • 与WaitForMultipleObjects()不同,这里不限制handles个数。
  • I/O completion ports 允许一个线程将一个请求暂时保存下来,而由另一个线程为它做实际服务。
  • I/O completion ports 默默支持 scalable 架构。

服务器的线程模型

有3 个基本的方法可以决定一个服务器上需要多少个线程:

  • 单独一个线程。 在一个文件服务器或一个简单的web服务器上,单一线程可以使用overlapped I/O来搬移数据,但是如果这个线程还必须进行任何其他操作,整个服务器就会陷入泥沼中。
  • 每个client 给予一个线程,如果你为每个client产生一个线程那么理论上每个人都可以有不错的反应时间,因为CPU的能量被平均分配了,然而事实上系统资源是有限的,到了某种情况下,系统效率就会急剧下降,比如如果有2000个client,这种做法就不切实际了。
  • 每个CPU给予一个线程,这种做法让每一个CPU尽可能忙碌,不至于出现哪一个CPU有过度饱和的情况,如果你使用event对象或APCs,此法将难以实现,因为它们都是否紧密地和某个线程绑在一起,此时我们就需要依赖一种特殊的同步对象,也就是I/O completion ports。

I/O completion port 是一种非常特殊的核心对象,用来综合一堆线程,让它们为overlapped请求服务。
其所提供的功能可以跨越多个CPUs。

Completion Ports 做了什么

IOCP允许你将启动overlapped 请求的线程提供服务的线程拆伙。
为了使用IOCP,你的程序应该产生一堆线程,统统在IOCP上等待着,这些线程都将成为能够处理completed overlapped I/O request的线程之一 ——只要线程在I/O completion port上等待,它就自然而然成为了那种线程。

每次有新的文件因为overlapped I/O开启你就可以让它的文件handle和I/O completion port产生关联,一旦这样的关系建立起来,任何文件操作如果成功完成,便会导致I/O completion packet 被送到 completion port去。这是发生操作系统之内的操作,对应用程序而言是透明的。

为了回应I/O completion packet,completion port释放了一个等待中的线程,如果目前没有线程正在等待,completion port就不会产生新线程。

释放出来的线程被给予足够的信息后就可以执行,处理该操作请求,但它还是属于原来的那一堆(被指定给此completion port)的线程,所不同的是,之前它是等待中的线程(waiting),现在它是一个作用中的线程(active),当这个线程将overlapped I/O请求处理完毕后,它应该再次在这个I/O completion port上等待。

现在我们再来描述一个completion port ,它是一个机制,用来管理一堆线程如何为completed overlapped I/O request服务。然而,completion port远比一个简单的分派器要丰富的多,它可以保持一个CPU或多个CPUs尽可能忙碌,但也避免它们被太多线程淹没,I/O completion port企图保持并行处理的线程个数在某个数字左右,一般而言你希望所有的CPUs都忙碌,所以一般而言并行处理的线程个数就是CPUs的个数。

I/O completion port运作过程中令人迷惑的部分就是,当一个线程被阻塞时它将发出通过并提交另一个线程。假设在单一CPU系统中有两个线程都正在一个I/O completion port上等待,线程1被唤醒,并且从网络上活得一包数据,为了服务这个数据包,线程1必须从磁盘上读取一个文件,所以它调用CreateFile()和ReadFile(),但是并不在overlapped模式中。Completion port 于是通告说线程1被磁盘I/O滞留了,并且提交线程2以使目前作用的线程个数到达需求数量。
当线程1 从磁盘操作中返回时,或许现有有两个线程同时在执行——甚至即使作用中的线程个数应该是1(因为CPU的个数只是1),这一行为令人惊讶,但却是正确的。Completion Port不会再提交另一个线程,除非作用中的线程个数再次降低到1以下。

操作概观

使用一个completion port 的快速摘要如下:
1. 产生一个I/O completion port
2. 让它和一个文件handle 产生关联
3. 产生一堆线程
4. 让每一个线程都在completion port 上等待
5. 开始对着那个文件handle 发出一些overlapped I/O 请求

当新文件被开启时,它们可以在任何时候与I/O completion port 产生关联。在completion port上等待的线程不应该做为completion port 服务以外的事情,因为这些线程一直都将是completion port 所持续追踪的那一堆线程的一部分。

步骤1:产生一个I/O Completion Port

I/O completion port 是一核心对象,你必须使用CreateIoCompletionPort()才能产生它:

HANDLE WINAPI CreateIoCompletionPort(
  _In_     HANDLE    FileHandle, //文件或设备的handle,可以使用INVALID_HANDLE_VALUE,表示产生一个与任何文件handle都无关的port
  _In_opt_ HANDLE    ExistingCompletionPort,//如果被指定,那么上一栏的FileHandle就会被加到这个port上,指定NULL可以产生一个新的port
  _In_     ULONG_PTR CompletionKey,//用户自定义的的一个数值,将被交给提供服务的线程,这个值和FileHandle有关
  _In_     DWORD     NumberOfConcurrentThreads//与此completion port 有关联的线程个数
);

返回值:
如果函数成功将返回一个I/O completion port的handle,如果函数失败那么则传回FALSE

任何文件只要附着到一个I/O completion port身上,都必须先以FILE_FLAG_OVERLAPPED开启,如果已经附着上去,就不能够再以ReadFileEx()或WriteFileEx()操作它。你可以任意关闭这样一个文件,没有安全上的顾虑。

还有需要注意的是,最后一个参数:
If this parameter is zero, the system allows as many concurrently running threads as there are processors in the system.
如果你设定为0,那么在CPU系统上就会有尽可能多的线程运行起来。

步骤2:与一个文件handle产生关联

CreateCompletionPort()通常被调用两次,一次先指定FileHandle为INVALID_HANDLE_VALUE,并设定ExistingCompletionPort 为NULL,用以产生一个port。
然后再为每一个欲附着上去的文件handle调用一次CreateIoCompletionPort(), 这些调用将ExistingCompletionPort设定为第一次调用所传回的handle。

例如:

HANDLE hPort;
HANDLE hFiles[MAX_FILES];
int index;
hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);
for(index =0; index<OPEN_FILES; index++){
    CreateIoCompletionPort(hFiles[index],hPort,0,0);
}

步骤3 : 产生一堆线程

一旦completion port 产生出来,你就可以设立在该port上等待的那些线程了。 I/O completion port并不自己产生那些线程,它只是使用由你产生的线程。因此你必须自己以CreateThread() 或_beginthreadex()或AfxBeginThread()产生出线程。
当你一产生这些线程时,它们都应该在completion port上等待,当线程开始为各个请求服务时,池子里的线程的组织如下:

  目前正在执行的线程
+ 被阻塞的线程
+ 在completion port上等待的线程
----------------------------------------
  池子里所有线程的个数

因为如此,所以你应该产生比CPU个数还多的线程,如果你只有一个CPU,而你也只产生了一个线程,那么当该线程阻塞时,你的CPU也变成闲置的了。由于池子里没有其他线程,completion port 也就没有办法为任何数据包服务,甚至即使CPU的能量游刃有余。

合理的线程个数应该是CPU个数的两倍加上2,你当然也可以产生更多的线程,但记住线程不是免费的。

步骤4 :在一个I/O Completion Port 上等待

work线程初始化自己后,它应该调用GetQueuedCompletionStatus(),这个操作像是WaitForSingleObject() 和 GetOverlappedResult()的组合。函数的规格如下:

BOOL WINAPI GetQueuedCompletionStatus(
  _In_  HANDLE       CompletionPort,//将在其上等待的completion port
  _Out_ LPDWORD      lpNumberOfBytes,//一个指针,指向被传输的数据字节数
  _Out_ PULONG_PTR   lpCompletionKey,//一个指针,由CreateIoCompletionPort()所定义的key
  _Out_ LPOVERLAPPED *lpOverlapped,//其实应该命名为lplpOverlapped,因为它指向的是overlapped结构的指针的地址
  _In_  DWORD        dwMilliseconds//等待的最长时间
);

返回值
如果函数成功将一个completion packet 从队列中取出,并完成一个成功的操作,函数将传回TRUE,并填写由lpNumberOfBytesTransferred 、lpCompletionKey、lpOverlapped所指向的变量内容。
如果操作失败,但completion packet已经从队列中取出,则函数传回FALSE,lpOverlapped 指向失败之操作。调用GetLastError()可获知为什么I/O操作会失败。
如果函数失败,则传回FALSE,lpOverlapped 设为NULL。

与其他的核心对象不同,在completion port上等待的线程是以先进后出(FILO)的次序提供服务。没有什么理由需要担忧次序的公平性,因为所有的线程都做完全相同的事情。
使用FILO,一个进行中的线程调用GetQueuedCompletionStatus()就可以取得下一个请求request,并保持执行状态,没有阻塞,这是非常有效率的。如果线程等待太长的时间,就有可能被置换出去(page out)。最近执行过的线程则通常还在内存中,不需要先置换进来才能执行。

步骤5: 发出overlapped I/O请求

这些请求可以启动一个能被I/O completion port所驾驭的I/O操作:

  • ConnectNamePipe()
  • DeviceIoControl()
  • LockFileEx()
  • ReadFile()
  • TransactNamePipe()
  • WaitCommEvent()
  • WriteFile()

为了使用completion port ,主线程可以对着一个与此completion port 有关联的文件,进行读、写、或其他任何操作。该线程不需要调用WaitForMultipleObjects(),因为池子里各个线程都调用过GetQueuedCompletionStatus()。一旦I/O操作完成,一个等待中的线程将会自动被释放,以服务该操作。

避免Completion Packets

常常会有这种情况:你读一个文件,但是操作完成时,你并不希望I/O completion port 被通告。网络服务器就是一个例子,在那里,线程从一个文件读进一个针对named pipe 或socket的请求,然后将回应写到同一个文件中。问题是文件已经以overlapped I/O状态打开了,所以写入操作将被异步。而当写入操作完成时,completion port将会收到一个completion packet,如果写入操作不是很重要,那么服务器花费许多时间处理不怎么重要的completion packets就显得得不偿失。
解决之道,将每个操作均会引发一个completion port 通告的开关关闭。

我们可以用受激发的event对象的机制取而代之,你设定一个overlapped结构,内含一个manual reset event对象,放在hEvent一栏,然后把handle最低位设为1. ————有点类似于hack行为,但是文件中是这么交代的。

OV
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值