完成端口详细解析.docx
- 文档编号:8154036
- 上传时间:2023-01-29
- 格式:DOCX
- 页数:19
- 大小:140.61KB
完成端口详细解析.docx
《完成端口详细解析.docx》由会员分享,可在线阅读,更多相关《完成端口详细解析.docx(19页珍藏版)》请在冰豆网上搜索。
完成端口详细解析
关于完成端口(IOCP)的文章汇总-[C/C++]
版权声明:
转载时请以超链接形式标明文章原始出处和作者信息及本声明
首先讨论一下I/OCompletionPorts试图解决什么样的问题。
写一个IOIntensive服务器程序,对每一个客户请求生成一个新的childprocess/workerthread来处理,每个process/thread使用同步IO,这是最经典古老的解法了。
在这之上的改进是prefork多个process或者使用线程池。
(使用process或thread,原理都差不多,thread的contextswitch花销要比processswitch要小。
为了论述简单,下面只讨论线程。
)
这种结构的并发性并不高,哪怕你用C++,C甚至汇编来写,效率都不会很高,究其原因,在于两点:
一.同步IO,每个线程大多数时间在等IOrequest的结束。
IO相对于CPU,那是极极慢的。
我翻了翻手里的ComputerArchitecture,AQuantitativeApproach第二版,1996年出的,里面对CPURegister,CPUCache,RAM,Disk,列的accesstime如下:
Java代码
1.Registers:
2-5nanoseconds
2.CPUCache:
3-10nanoseconds
3.RAM:
80-400nanoseconds
4.Disk:
5000000nanoseconds(5milliseconds)
如今CPU又按照摩尔定律发展了十年后,这个硬盘还是机械式的磁头移来移去读写,尽管如今diskcontroller都有cache,也在发展,但和CPU相比,差距越来越大。
(谁有最新数据可以贴上来。
)
二.生成数量大大超过CPU总数的线程。
这样做有两个弊端,第一是每个线程要占用内存,Windows底下每个thread自己stack的省缺大小为1M,32位程序下一个用户程序最大能利用的内存也就3G,生成3000个线程,内存就没了。
当然有人说64位下面,可以随便浪费,那么,第二个弊端,就无法避免了─生成大量的线程,CPU必然会花费大量的cpucycles在线程之间进行切换。
如今市场上价格适中的服务器也就2cpux4core=8核而已。
生成那么多的线程,CPU在切换线程上花的功夫可能比干正经事还要多。
明白了原因,就可以寻找改进方法。
首先,使用异步IO。
现在所有主流OS,都提供异步IO(non-blockingIO),连Java这种跨平台的编程环境都在版本1.4里开始支持异步IO了。
但是,光有异步IO,这是不够的。
论坛里有人发贴子问过,“我的线程发个IORequest,异步IO,直接返回了,然后我的线程干什么?
”异步IO是操作系统提供的机制,我们还需要设计我们程序的结构,使异步IO和线程结合起来,可以充分利用异步IO带来的好处,同时必须控制同时运行线程的数量,减少threadcontextswitch的开销。
IOCompletionPort,是微软针对上述思想,在Windows内核级别,提供的解决方案。
从抽象高度去理解IOCompletionPort,可以把它想成一个magicport,一边有一个队列是IO驱动程序处理好的IO数据,另一边是一个小小的线程池,这个port把io数据交给线程池里的线程来处理。
同时,别的线程启动了IO异步请求后通知这个port一声,“嘿,注意了,一会儿这个IOhandle会有个数据包传过来要处理。
”这个port回答,“好,我注意一下这个handle。
”。
下面我们具体看一下IoCompletionPort这个内核对象以及使用。
要创建IoCompletionPort,呼叫Win32函数CreateIoCompletionPort。
这个函数一身两用,创建IoCompletionPort也是它,往建好的IoCompletionPort里面加devicehandle也是它。
Java代码
1.HANDLECreateIoCompletionPort(
2.HANDLEhfile,
3.HANDLEhExistingCompPort,
4.ULONG_PTRCompKey,
5.DWORDdwNumberOfConcurrentThreads);
6.
7.//创建IoCompletionPort
8.
9.HANDLEhCp;
10.hCp=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,4);
创建IoCompletionPort头三个参数都是NULL之类的,只有第四个参数,用来配置这个生成的IoCompletionPort所允许同时运行的最大线程数目。
创建好的IoCompletionPortkernelobject,拥有两个队列。
一个是DeviceList,包含所有通过这个IoCompletionPort管理的异步IO请求的DeviceHandle。
另外一个是I/OCompletionQueue(FIFO),DeviceHandle对应的IO驱动程序处理好的IO数据,放在这个队列里。
为了能在DeviceList这个队列里面加个entry,用户程序将再一次使用CreateIoCompletionPort这个函数。
Java代码
1.CreateIoCompletionPort(myHandle,hCp,myKey,0);
第一个参数是个IOHandle(Windows不限制handle类型,File,Directory,SerialPort,Parallelport,Mailslotserver,Mailslotclient,pipe,socket等等都可以),第二个参数是以前创建的IoCompletionPorthandle.这个IOHandle将放到IoCompletionPorthandle的DeviceList那个队列里去。
第三个参数是个long整数,是用来identify程序RequestContext的。
因为现在程序不是一个线程来处理客户Request了,而是不同的线程来处理。
在一个线程里按顺序一二三四五来实现程序逻辑的方式是不行了,因此作为程序员你要把逻辑的Context记下来,让不同的线程得到这个Context,根据当前的状态,来执行相关的代码。
这个completionkey,是找到相映的context的key,index,hashcode,pointer,whatever.
第二个队列,IOCompletionQueue,是由OS往里面插入entry的。
OS在处理好了IO异步请求之后,察看一下这个Devicehandle是否是放在某个CompletionPort里面,如果是,OS就在CompletionPort的CompletionQueue里面加个Entry。
这个Entry包括下列数据。
Java代码
1.1.Numberofbytestransferred
2.2.Completionkey
3.3.PointertoI/Orequest’sOVERLAPPEDstructure
4.4.Errorcode
下面来看看IOCompletionPort是怎么管理线程的。
前面说CompletionPort有个线程池,这种说法并不是很贴切。
CompletionPort本身并不创建线程,而只是掌管三个thread队列:
Java代码
1.1.InactivethreadswaitsIOCompletionPort
2.2.Activerunningthreads
3.3.Threadspausedbyotherreasons,likewaitingforsomethingelse(i.e.callsWaitForSingleObject,orevenstupidbutvalid,callsSleep).
线程由程序创建,然后加入第一个队列(waitsonIOCompletionPort)。
为了加入这个队列线程要呼叫一个函数,GetQueuedCompletionStatus。
Java代码
1.BOOLGetQueuedCompletionStatus(
2.HANDLEhCompPort,
3.PDWORDpdwNumBytes,
4.PULONG_PTRCompKey,
5.OVERLAPPED**ppOverlapped,
6.DWORDdwMilliseconds);
第一个参数是handletoCompletionPort,线程通知OS本线程要加入这个CompletionPort的第一个队列。
这个函数会block当前线程,使其处于inactive状态。
现在再去看看图二的I/OCompletionQueue,OS在一份IO异步请求处理好了后,会在这里插入个entry,CompletionPort在收到entry后,看看线程池里面有没有空闲没事做的线程,如果有,不要忘记我们创建这个CompletionPort时候规定了个最大同时运行线程数量,如果当前运行线程数量小于这个最大值,那么就把这个线程放到第二个(activerunning)的队列上去,让这个线程运行起来。
前面不是说线程在GetQueuedCompletionStatus上面block了么,现在这个函数返回了,继续运行程序的代码。
通过这个最大同时运行线程数量,保证了不会有太多的线程在运行,Viola!
本文开头分析的几个问题全解决了。
即是异步IO,又把异步IO和线程池结合了起来,还控制了当前运行线程数量。
It’sBEAUTIFUL!
这个线程处理完程序逻辑后,呼叫一下GetQueuedCompletionStatus,又回到了第一个队列。
有意思的是这个队列的逻辑是LastInFirstOut。
如果又有IO数据等待线程处理,这个线程可以继续执行,不用进行ContextSwitch,典型的能者多劳啊,越能干的人干的越多。
这个线程在处理程序逻辑的过程中,可能会因为别的原因而变成inactive,比如在等别的资源(WaitForSingleObject),或者变态一点,自己来了个Sleep,这时线程就给放到第三个队列去了。
这里有个有趣的现象,假如开始我们在第一个队列里面放三个线程,而最大同时运行线程数量设为2,在两个线程跑起来之后,第三个就不跑了,如果这时运行中的某个线程因为等别的资源而变为inactive,那么第三个线程也开始跑起来,同时运行线程数量还是2,这时那个等别的资源的线程等到资源了,又开始跑了起来,这时同时运行线程数量就是3,比设定的2要大。
推荐的最大同时运行线程数量一般为CPU的总数,但是如果运行的线程还要等别的资源,建议把这个数目稍微设大一点,这样并发率会更高.关于这一点微软的描述如下:
AnI/Ocompletionportisassociatedwiththeprocessthatcreateditandisnotshareablebetweenprocesses.However,asinglehandleisshareablebetweenthreadsinthesameprocess.ForanotherarticleaboutI/Ocompletionports,see"InsideI/OCompletionPorts"intheMicrosoftTechNetLibraryat
ThreadsandConcurrency
ThemostimportantpropertyofanI/Ocompletionporttoconsidercarefullyistheconcurrencyvalue.TheconcurrencyvalueofacompletionportisspecifiedwhenitiscreatedwithCreateIoCompletionPortviatheNumberOfConcurrentThreadsparameter.Thisvaluelimitsthenumberofrunnablethreadsassociatedwiththecompletionport.Whenthetotalnumberofrunnablethreadsassociatedwiththecompletionportreachestheconcurrencyvalue,thesystemblockstheexecutionofanysubsequentthreadsassociatedwiththatcompletionportuntilthenumberofrunnablethreadsdropsbelowtheconcurrencyvalue.
Themostefficientscenariooccurswhentherearecompletionpacketswaitinginthequeue,butnowaitscanbesatisfiedbecausetheporthasreacheditsconcurrencylimit.ConsiderwhathappenswithaconcurrencyvalueofoneandmultiplethreadswaitingintheGetQueuedCompletionStatusfunctioncall.Inthiscase,ifthequeuealwayshascompletionpacketswaiting,whentherunningthreadcallsGetQueuedCompletionStatus,itwillnotblockexecutionbecause,asmentionedearlier,thethreadqueueisLIFO.Instead,thisthreadwillimmediatelypickupthenextqueuedcompletionpacket.Nothreadcontextswitcheswilloccur,becausetherunningthreadiscontinuallypickingupcompletionpacketsandtheotherthreadsareunabletorun.
Note
Inthepreviousexample,theextrathreadsappeartobeuselessandneverrun,butthatassumesthattherunningthreadnevergetsputinawaitstatebysomeothermechanism,terminates,orotherwiseclosesitsassociatedI/Ocompletionport.Considerallsuchthreadexecutionramificationswhendesigningtheapplication.
ThebestoverallmaximumvaluetopickfortheconcurrencyvalueisthenumberofCPUsonthecomputer.Ifyourtransactionrequiredalengthycomputation,alargerconcurrencyvaluewillallowmorethreadstorun.Eachcompletionpacketmaytakelongertofinish,butmorecompletionpacketswillbeprocessedatthesametime.Youcanexperimentwiththeconcurrencyvalueinconjunctionwithprofilingtoolstoachievethebesteffectforyourapplication.
ThesystemalsoallowsathreadwaitinginGetQueuedCompletionStatustoprocessacompletionpacketifanotherrunningthreadassociatedwiththesameI/Ocompletionportentersawaitstateforotherreasons,forexampletheSuspendThreadfunction.Whenthethreadinthewaitstatebeginsrunningagain,theremaybeabriefperiodwhenthenumberofactivethreadsexceedstheconcurrencyvalue.However,thesystemquicklyreducesthisnumberbynotallowinganynewactivethreadsuntilthenumberofactivethreadsfallsbelowtheconcurrencyvalue.Thisisonereasontohaveyourapplicationcreatemorethreadsinitsthreadpoolthantheconcurrencyvalue.Threadpoolmanagementisbeyondthescopeofthistopic,butagoodruleofthumbistohaveaminimumoftwiceasmanythreadsinthethreadpoolasthereareprocessorsonthesystem.Foradditionalinformationaboutthreadpooling,seeThreadPools.
关于完成端口的另一个应用:
ThreadscanusethePostQueuedCompletionStatusfunctiontoplacecompletionpacketsinanI/Ocompletionport'squeue.Bydoingso,thecompletionportcanbeusedtoreceivecommunicationsfromotherthreadsoftheprocess,inadditiontoreceivingI/OcompletionpacketsfromtheI/Osystem.ThePostQueuedCompletionStatusfunctionallowsanapplicationtoqueueitsownspecial-purposecompletionpacketstotheI/OcompletionportwithoutstartinganasynchronousI/Ooperation.Thisisusefulfornotifyingworkerthreadsofexternalevents,forexample.
完成端口的问题
异步过程调用(apcs)问题:
只有发overlapped请求的线程才可以提供callback函数(需要一个特定的线程为一个特定的I/O请求服务)。
完成端口(I/Ocompletion)的优点:
不会限制handle个数,可处理成千上万个连接。
I/Ocompletionport允许一个线程将一个请求暂时保存下来,由另一个线程为它做实际服务。
并发模型与线程池:
在典型的并发模型中,服务器为每一个客户端创建一个线程,如果很多客户同时请求,则这些线程都是运行的,那么CPU就要一个个切换,CPU花费了更多的时间在线程切换,线程确没得到很多CPU时间。
到底应该创建多少个线程比较合适呢,微软件帮助文档上讲应该是2*CPU个。
但理想条件下最好线程不要切换,而又能象线程池一样,重复利用。
I/O完成端口就是使用了线程池。
理解与使用:
第一步:
在我们使用完成端口之前,要调用CreateIoCompletionPort函数先创建完成端口对象。
定义如下:
HANDLECreateIoCompletionPort(
HANDLEFileHandle,
HANDLEExistingCompletionPort,
DWORDCompletionKey,
DWORDNumberOfConcurrentThreads
);
FileHandle:
文件或设备的handle,如果值为INVALID_HANDLE_VALUE则产生一个没有和任何文件handle有关系的port.(可以用来和完成端口联系的各种句柄,文件,套接字)
ExistingCompletionPort:
NULL时生成一个新port,否则handle会加到此port上。
CompletionKey:
用户自定义数值,被交给服务的线程。
GetQueuedCompletionStatus函数时我们可以完全得到我们在此联系函数中的完成键(申请的内存块)。
在GetQueuedCompletionStatus
中可以完封不动的得到这个内存块,并且使用它。
NumberOfConcurrentThreads:
参数NumberOfConcurrentThreads用来指定在一个完成端口上可以并发的线程数量。
理想的情况是,一个处理器上只
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 完成 端口 详细 解析