Re: [Windows]I/O completion ports如何实现?个人推测,寻求标准答案! Windows多线程VisioOSthread
程序员文章站
2022-03-14 17:28:37
...
这是对http://www.iteye.com/topic/102007的一个回帖,颇受好评,得了五星,加了若干分,故收入博客。
基本没什么大错误,经iunknown指正,对non-blocking IO的理解有误。等闲一点,好好翻翻书再改正吧。
--------------------------------
我来侃一侃我对I/O Completion Ports的理解吧。这是一个比较复杂的话题,下面的描述尽量详细,争取把来龙去脉讲清楚,勿嫌啰嗦。
首先讨论一下I/O Completion Ports试图解决什么样的问题。
写一个IO Intensive服务器程序,对每一个客户请求生成一个新的child process/worker thread来处理,每个process/thread使用同步IO,这是最经典古老的解法了。在这之上的改进是prefork 多个process 或者使用线程池。(使用process或thread,原理都差不多,thread的context switch花销要比process switch要小。为了论述简单,下面只讨论线程。)
这种结构的并发性并不高,哪怕你用C++, C甚至汇编来写,效率都不会很高,究其原因,在于两点:
一.同步IO,每个线程大多数时间在等IO request的结束。IO相对于CPU,那是极极慢的。我翻了翻手里的Computer Architecture, A Quantitative Approach第二版,1996年出的,里面对CPU Register, CPU Cache, RAM, Disk,列的access time如下:
如今CPU又按照摩尔定律发展了十年后,这个硬盘还是机械式的磁头移来移去读写,尽管如今disk controller都有cache,也在发展,但和CPU相比,差距越来越大。(谁有最新数据可以贴上来。)
二.生成数量大大超过CPU总数的线程。这样做有两个弊端,第一是每个线程要占用内存,Windows底下每个thread自己stack的省缺大小为1M,32位程序下一个用户程序最大能利用的内存也就3G,生成3000个线程,内存就没了。当然有人说64位下面,可以随便浪费,那么,第二个弊端,就无法避免了 ─ 生成大量的线程,CPU必然会花费大量的cpu cycles在线程之间进行切换。如今市场上价格适中的服务器也就2 cpu x 4 core = 8 核而已。生成那么多的线程,CPU在切换线程上花的功夫可能比干正经事还要多。
明白了原因,就可以寻找改进方法。首先,使用异步IO。现在所有主流OS,都提供异步IO(non-blocking IO),连Java这种跨平台的编程环境都在版本1.4里开始支持异步IO了。但是,光有异步IO,这是不够的。论坛里有人发贴子问过,“我的线程发个IO Request,异步IO,直接返回了,然后我的线程干什么?” 异步IO是操作系统提供的机制,我们还需要设计我们程序的结构,使异步IO和线程结合起来,可以充分利用异步IO带来的好处,同时必须控制同时运行线程的数量,减少thread context switch的开销。
IO Completion Port, 是微软针对上述思想,在Windows内核级别,提供的解决方案。
从抽象高度去理解IO Completion Port,可以把它想成一个magic port,一边有一个队列是IO驱动程序处理好的IO数据,另一边是一个小小的线程池,这个port把io数据交给线程池里的线程来处理。同时,别的线程启动了IO异步请求后通知这个port一声,“嘿,注意了,一会儿这个IO handle 会有个数据包传过来要处理。” 这个port回答,“好,我注意一下这个handle。”。
下面我们具体看一下Io Completion Port这个内核对象以及使用。
要创建IoCompletionPort,呼叫Win32函数CreateIoCompletionPort。这个函数一身两用,创建IoCompletionPort也是它,往建好的IoCompletionPort里面加device handle也是它。
创建IoCompletionPort头三个参数都是NULL之类的,只有第四个参数,用来配置这个生成的IoCompletionPort所允许同时运行的最大线程数目。
创建好的IoCompletionPort kernel object,拥有两个队列。一个是Device List,包含所有通过这个IoCompletionPort管理的异步IO请求 的Device Handle。另外一个是I/O Completion Queue (FIFO),Device Handle对应的IO驱动程序处理好的IO数据,放在这个队列里。
为了能在Device List这个队列里面加个entry,用户程序将再一次使用CreateIoCompletionPort 这个函数。
第一个参数是个IO Handle(Windows不限制handle类型,File, Directory, Serial Port, Parallel port, Mailslot server, Mailslot client, pipe, socket等等都可以),第二个参数是以前创建的IoCompletionPort handle. 这个IO Handle将放到IoCompletionPort handle的Device List那个队列里去。第三个参数是个long整数,是用来identify程序Request Context的。因为现在程序不是一个线程来处理客户Request了,而是不同的线程来处理。在一个线程里按顺序一二三四五来实现程序逻辑的方式是不行了,因此作为程序员你要把逻辑的Context记下来,让不同的线程得到这个Context,根据当前的状态,来执行相关的代码。这个completion key,是找到相映的context的key, index, hash code,pointer, whatever.
第二个队列,IO Completion Queue,是由OS往里面插入entry的。OS在处理好了IO异步请求之后,察看一下这个Device handle是否是放在某个Completion Port里面,如果是,OS就在Completion Port的Completion Queue里面加个Entry。这个Entry包括下列数据。
下面来看看IO Completion Port是怎么管理线程的。
前面说Completion Port有个线程池,这种说法并不是很贴切。Completion Port本身并不创建线程,而只是掌管三个thread队列:
线程由程序创建,然后加入第一个队列(waits on IO Completion Port)。为了加入这个队列线程要呼叫一个函数,GetQueuedCompletionStatus。
第一个参数是handle to Completion Port,线程通知OS本线程要加入这个Completion Port的第一个队列。这个函数会block当前线程,使其处于inactive状态。
现在再去看看图二的I/O Completion Queue,OS在一份IO异步请求处理好了后,会在这里插入个entry,Completion Port在收到entry后,看看线程池里面有没有空闲没事做的线程,如果有,不要忘记我们创建这个Completion Port时候规定了个最大同时运行线程数量,如果当前运行线程数量小于这个最大值,那么就把这个线程放到第二个(active running)的队列上去,让这个线程运行起来。前面不是说线程在GetQueuedCompletionStatus上面block了么,现在这个函数返回了,继续运行程序的代码。通过这个最大同时运行线程数量,保证了不会有太多的线程在运行,Viola! 本文开头分析的几个问题全解决了。即是异步IO,又把异步IO和线程池结合了起来,还控制了当前运行线程数量。It’s BEAUTIFUL!
这个线程处理完程序逻辑后,呼叫一下GetQueuedCompletionStatus,又回到了第一个队列。有意思的是这个队列的逻辑是Last In First Out。如果又有IO数据等待线程处理,这个线程可以继续执行,不用进行Context Switch,典型的能者多劳啊,越能干的人干的越多。
这个线程在处理程序逻辑的过程中,可能会因为别的原因而变成inactive,比如在等别的资源(WaitForSingleObject),或者变态一点,自己来了个Sleep,这时线程就给放到第三个队列去了。
这里有个有趣的现象,假如开始我们在第一个队列里面放三个线程,而最大同时运行线程数量设为2,在两个线程跑起来之后,第三个就不跑了,如果这时运行中的某个线程因为等别的资源而变为inactive,那么第三个线程也开始跑起来,同时运行线程数量还是2,这时那个等别的资源的线程等到资源了,又开始跑了起来,这时同时运行线程数量就是3,比设定的2要大。
推荐的最大同时运行线程数量一般为CPU的总数,但是如果运行的线程还要等别的资源,建议把这个数目稍微设大一点,这样并发率会更高。
先写这么多吧,累死了。这个帖子讲述的是IO Completion Port的工作原理,还没有谈到它是如何实现的,也还没有扯到Windows Source Code. 如果对工作原理了解了,也未必要去深究是如何实现的。在没有IO Completion Port的系统上,自己实现一个,也未必不可。当然讨论讨论是如何实现的,也是件很有趣的事情。这方面我也没有完全搞个水落石出。让我先攒攒劲,改天再写吧。
另外搭个顺风车问个问题,我做图的水平很臭,手里就有个Visio还用不好。本文的图都是在Word里面画的。请问能否推荐个好用的作图软件,免费还是花钱的都可以。要有什么画UML比较好的软件,也推荐推荐,Visio太不好用了。
基本没什么大错误,经iunknown指正,对non-blocking IO的理解有误。等闲一点,好好翻翻书再改正吧。
--------------------------------
我来侃一侃我对I/O Completion Ports的理解吧。这是一个比较复杂的话题,下面的描述尽量详细,争取把来龙去脉讲清楚,勿嫌啰嗦。
首先讨论一下I/O Completion Ports试图解决什么样的问题。
写一个IO Intensive服务器程序,对每一个客户请求生成一个新的child process/worker thread来处理,每个process/thread使用同步IO,这是最经典古老的解法了。在这之上的改进是prefork 多个process 或者使用线程池。(使用process或thread,原理都差不多,thread的context switch花销要比process switch要小。为了论述简单,下面只讨论线程。)
这种结构的并发性并不高,哪怕你用C++, C甚至汇编来写,效率都不会很高,究其原因,在于两点:
一.同步IO,每个线程大多数时间在等IO request的结束。IO相对于CPU,那是极极慢的。我翻了翻手里的Computer Architecture, A Quantitative Approach第二版,1996年出的,里面对CPU Register, CPU Cache, RAM, Disk,列的access time如下:
Registers: 2-5 nano seconds CPU Cache: 3-10 nano seconds RAM: 80-400 nano seconds Disk: 5 000 000 nano seconds (5 milli seconds)
如今CPU又按照摩尔定律发展了十年后,这个硬盘还是机械式的磁头移来移去读写,尽管如今disk controller都有cache,也在发展,但和CPU相比,差距越来越大。(谁有最新数据可以贴上来。)
二.生成数量大大超过CPU总数的线程。这样做有两个弊端,第一是每个线程要占用内存,Windows底下每个thread自己stack的省缺大小为1M,32位程序下一个用户程序最大能利用的内存也就3G,生成3000个线程,内存就没了。当然有人说64位下面,可以随便浪费,那么,第二个弊端,就无法避免了 ─ 生成大量的线程,CPU必然会花费大量的cpu cycles在线程之间进行切换。如今市场上价格适中的服务器也就2 cpu x 4 core = 8 核而已。生成那么多的线程,CPU在切换线程上花的功夫可能比干正经事还要多。
明白了原因,就可以寻找改进方法。首先,使用异步IO。现在所有主流OS,都提供异步IO(non-blocking IO),连Java这种跨平台的编程环境都在版本1.4里开始支持异步IO了。但是,光有异步IO,这是不够的。论坛里有人发贴子问过,“我的线程发个IO Request,异步IO,直接返回了,然后我的线程干什么?” 异步IO是操作系统提供的机制,我们还需要设计我们程序的结构,使异步IO和线程结合起来,可以充分利用异步IO带来的好处,同时必须控制同时运行线程的数量,减少thread context switch的开销。
IO Completion Port, 是微软针对上述思想,在Windows内核级别,提供的解决方案。
从抽象高度去理解IO Completion Port,可以把它想成一个magic port,一边有一个队列是IO驱动程序处理好的IO数据,另一边是一个小小的线程池,这个port把io数据交给线程池里的线程来处理。同时,别的线程启动了IO异步请求后通知这个port一声,“嘿,注意了,一会儿这个IO handle 会有个数据包传过来要处理。” 这个port回答,“好,我注意一下这个handle。”。
下面我们具体看一下Io Completion Port这个内核对象以及使用。
要创建IoCompletionPort,呼叫Win32函数CreateIoCompletionPort。这个函数一身两用,创建IoCompletionPort也是它,往建好的IoCompletionPort里面加device handle也是它。
HANDLE CreateIoCompletionPort( HANDLE hfile, HANDLE hExistingCompPort, ULONG_PTR CompKey, DWORD dwNumberOfConcurrentThreads); // 创建IoCompletionPort HANDLE hCp; hCp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 4);
创建IoCompletionPort头三个参数都是NULL之类的,只有第四个参数,用来配置这个生成的IoCompletionPort所允许同时运行的最大线程数目。
创建好的IoCompletionPort kernel object,拥有两个队列。一个是Device List,包含所有通过这个IoCompletionPort管理的异步IO请求 的Device Handle。另外一个是I/O Completion Queue (FIFO),Device Handle对应的IO驱动程序处理好的IO数据,放在这个队列里。
为了能在Device List这个队列里面加个entry,用户程序将再一次使用CreateIoCompletionPort 这个函数。
CreateIoCompletionPort(myHandle, hCp, myKey, 0);
第一个参数是个IO Handle(Windows不限制handle类型,File, Directory, Serial Port, Parallel port, Mailslot server, Mailslot client, pipe, socket等等都可以),第二个参数是以前创建的IoCompletionPort handle. 这个IO Handle将放到IoCompletionPort handle的Device List那个队列里去。第三个参数是个long整数,是用来identify程序Request Context的。因为现在程序不是一个线程来处理客户Request了,而是不同的线程来处理。在一个线程里按顺序一二三四五来实现程序逻辑的方式是不行了,因此作为程序员你要把逻辑的Context记下来,让不同的线程得到这个Context,根据当前的状态,来执行相关的代码。这个completion key,是找到相映的context的key, index, hash code,pointer, whatever.
第二个队列,IO Completion Queue,是由OS往里面插入entry的。OS在处理好了IO异步请求之后,察看一下这个Device handle是否是放在某个Completion Port里面,如果是,OS就在Completion Port的Completion Queue里面加个Entry。这个Entry包括下列数据。
1.Number of bytes transferred 2.Completion key 3.Pointer to I/O request’s OVERLAPPED structure 4.Error code
下面来看看IO Completion Port是怎么管理线程的。
前面说Completion Port有个线程池,这种说法并不是很贴切。Completion Port本身并不创建线程,而只是掌管三个thread队列:
1.Inactive threads waits IO Completion Port 2.Active running threads 3.Threads paused by other reasons, like waiting for something else (i.e. calls WaitForSingleObject, or even stupid but valid, calls Sleep).
线程由程序创建,然后加入第一个队列(waits on IO Completion Port)。为了加入这个队列线程要呼叫一个函数,GetQueuedCompletionStatus。
BOOL GetQueuedCompletionStatus( HANDLE hCompPort, PDWORD pdwNumBytes, PULONG_PTR CompKey, OVERLAPPED** ppOverlapped, DWORD dwMilliseconds);
第一个参数是handle to Completion Port,线程通知OS本线程要加入这个Completion Port的第一个队列。这个函数会block当前线程,使其处于inactive状态。
现在再去看看图二的I/O Completion Queue,OS在一份IO异步请求处理好了后,会在这里插入个entry,Completion Port在收到entry后,看看线程池里面有没有空闲没事做的线程,如果有,不要忘记我们创建这个Completion Port时候规定了个最大同时运行线程数量,如果当前运行线程数量小于这个最大值,那么就把这个线程放到第二个(active running)的队列上去,让这个线程运行起来。前面不是说线程在GetQueuedCompletionStatus上面block了么,现在这个函数返回了,继续运行程序的代码。通过这个最大同时运行线程数量,保证了不会有太多的线程在运行,Viola! 本文开头分析的几个问题全解决了。即是异步IO,又把异步IO和线程池结合了起来,还控制了当前运行线程数量。It’s BEAUTIFUL!
这个线程处理完程序逻辑后,呼叫一下GetQueuedCompletionStatus,又回到了第一个队列。有意思的是这个队列的逻辑是Last In First Out。如果又有IO数据等待线程处理,这个线程可以继续执行,不用进行Context Switch,典型的能者多劳啊,越能干的人干的越多。
这个线程在处理程序逻辑的过程中,可能会因为别的原因而变成inactive,比如在等别的资源(WaitForSingleObject),或者变态一点,自己来了个Sleep,这时线程就给放到第三个队列去了。
这里有个有趣的现象,假如开始我们在第一个队列里面放三个线程,而最大同时运行线程数量设为2,在两个线程跑起来之后,第三个就不跑了,如果这时运行中的某个线程因为等别的资源而变为inactive,那么第三个线程也开始跑起来,同时运行线程数量还是2,这时那个等别的资源的线程等到资源了,又开始跑了起来,这时同时运行线程数量就是3,比设定的2要大。
推荐的最大同时运行线程数量一般为CPU的总数,但是如果运行的线程还要等别的资源,建议把这个数目稍微设大一点,这样并发率会更高。
先写这么多吧,累死了。这个帖子讲述的是IO Completion Port的工作原理,还没有谈到它是如何实现的,也还没有扯到Windows Source Code. 如果对工作原理了解了,也未必要去深究是如何实现的。在没有IO Completion Port的系统上,自己实现一个,也未必不可。当然讨论讨论是如何实现的,也是件很有趣的事情。这方面我也没有完全搞个水落石出。让我先攒攒劲,改天再写吧。
另外搭个顺风车问个问题,我做图的水平很臭,手里就有个Visio还用不好。本文的图都是在Word里面画的。请问能否推荐个好用的作图软件,免费还是花钱的都可以。要有什么画UML比较好的软件,也推荐推荐,Visio太不好用了。
上一篇: jquery获取时间方法总结