none
关于.net下socket完成端口的问题[clr异步处理机制] RRS feed

  • 问题

  • 大家好,我想问下.net中的io线程池和window下io完成端口的io等待线程之间是什么关系?

    我们知道windows下io完成端口的io等待线程的数目很少,一般设置成cpu个数。在这些线程里面处理io完成队列上的io完成包,每当有异步io完成了都会在io等待线程上拿包处理。这里的io等待线程在while(1)循环中调用GetQueuedCompletionStatus方法阻塞拿包,而且最后调用这个方法的线程最先拿包处理,以一种栈的方式后入先出的从io完成队列上拿包,从而最大程度避免线程切换。

    第一个问题:

    .net下面的异步io操作是对windows下iocp的封装,让我不明白的是.net下的io线程池默认线程数1000个,那么这个里面的io线程和在windows下的io等待线程有什么关系?看过了<<clr via c#>>里面说的貌似clr中的io线程池就是windows下的io等待线程。但是这个默认数1000是怎么回事呢?让我不解。

    第二个问题:

    还有如果io线程池就是windows下的io等待线程,那么如果拿到io完成包后的处理是一个比较耗时间的业务计算过程,打比方说要5秒,我们对这种耗时的处理是不是不应该放在io等待线程中呢,因为io等待线程数太少(如果此时并发访问的数量已经把io等待线程全部耗尽,但是业务计算都没完成,此时是不能处理新的io完成包的),是不是应该放到另外的工作线程中处理?比如将拿到的io完成包放入一个队列中,然后线程池从队列中拿包处理,这样的处理机制是不是更合理。

    希望我描述清楚了我的两个问题,请童鞋们帮忙解答。

    2013年12月12日 1:00

答案

  • 我在 cnblogs 上已经回答过你这个问题。

    我这里再啰嗦几句。

    1,1000,是个经验值,除了有上限,还有下限,这个下限就是你开头提到的“cpu个数”。这个下限是预分配的用于在GetQueuedCompletionStatus 上等待的线程数目,上限是表示,如果创建的线程都在运行中,那么还可以创建小于上限数目的线程用于等待 GetQueuedCompletionStatus 上即将到来的新的完成包,避免 CPU 不够饱和。

    2,如果你是 CPU 密集型的操作,上限设置再大都没有用,因为实际并行的线程受逻辑处理器个数限制,但是因为就算是线程在执行 CPU 密集型操作,它也可能让出时间片,因此上限会分配的稍微多点,比如逻辑处理器个数加 2 。这样保证当某个线程让出时间片后,有一个可用的线程能够继续占满当前逻辑处理器的时间片。因此你提到的解决办法并不是有效的,这依据你的业务计算的复杂度而定。所以线程池模型是允许你修改上限和下限的(通常不修改下限),上限修改成多少,你应该通过实际的测试数据来不断的微调,以到达吞吐最大,并且 CPU 饱和。


    2013年12月12日 2:39

全部回复

  • 我在 cnblogs 上已经回答过你这个问题。

    我这里再啰嗦几句。

    1,1000,是个经验值,除了有上限,还有下限,这个下限就是你开头提到的“cpu个数”。这个下限是预分配的用于在GetQueuedCompletionStatus 上等待的线程数目,上限是表示,如果创建的线程都在运行中,那么还可以创建小于上限数目的线程用于等待 GetQueuedCompletionStatus 上即将到来的新的完成包,避免 CPU 不够饱和。

    2,如果你是 CPU 密集型的操作,上限设置再大都没有用,因为实际并行的线程受逻辑处理器个数限制,但是因为就算是线程在执行 CPU 密集型操作,它也可能让出时间片,因此上限会分配的稍微多点,比如逻辑处理器个数加 2 。这样保证当某个线程让出时间片后,有一个可用的线程能够继续占满当前逻辑处理器的时间片。因此你提到的解决办法并不是有效的,这依据你的业务计算的复杂度而定。所以线程池模型是允许你修改上限和下限的(通常不修改下限),上限修改成多少,你应该通过实际的测试数据来不断的微调,以到达吞吐最大,并且 CPU 饱和。


    2013年12月12日 2:39
  • 查了很多资料,终于大概明白是怎么回事了

    在完成端口上实际有3个线程队列:

    等待线程队列:调用GetQueuedCompletionStatus后阻塞的线程

    已释放线程队列:从等待线程中唤醒的拿到io完成包的正在运行状态的线程

    已暂停线程队列:已释放线程中调用阻塞的方法,例如Thread.Sleep,会从释放线程队列移到暂停队列中,暂停结束后会回到释放线程队列

    完成端口的处理逻辑是这样的:当完成队列上有io完成数据包的时候,系统会检查已释放线程队列中的数目,如果小于CreateIoCompletionPort建立完成端口时的参数dwNumberOfConcurrentThreads指定的最大并发线程数那么可以从等待线程队列中唤醒一个线程处理,这个线程会移到已释放队列中。而如果已释放线程队列中的数目大于或者等于(大于是因为从暂停队列中回来)dwNumberOfConcurrentThreads,这个时候是不会从等待线程队列中唤醒线程处理的。

    clr中的io线程池就是对待在io完成端口上的线程,而这个初始线程数目就是你说到的线程下限数目,如果io线程切换到等待状态,例如Thread.Sleep,那么当前唤醒的这个io线程会移到已暂停线程队列上,也就是说已释放线程队列中数目减1,这个时候又可以从等待线程队列中唤醒新的io线程了,这样的结果就是当那个暂停的线程恢复运行回到释放线程队列的时候短时间内可能已释放线程队列中数目大于dwNumberOfConcurrentThreads,这个时候系统不会唤醒新的io线程,直到这个数目小于dwNumberOfConcurrentThreads。

    而这三个线程队列中的线程都来自clr中的io线程池。

    如果是cpu密集型且耗时过长的回调,多并发时会导致已释放线程队列数目达到dwNumberOfConcurrentThreads从而饱和,这个时候不能处理新的io完成包了,因为在已释放线程队列有处理完成前不会有新的io线程唤醒。

    大概原理是不是这样呢:)

    2013年12月12日 10:16
  • 大概就是这么个意思,你可以看一下 ATL 的 CThreadPool 类,这是一个使用完成端口实现的线程池,只是它在修改池大小时非常鲁莽。

    另外提一点,耗时过长是导致饱和的唯一原因,跟是 CPU 密集型,还是 IO 密集型操作无关。而 CPU 密集型和 IO 密集型操作只是用来区分增加池大小,是否有助于提高性能。

    • 已编辑 Skyseer 2013年12月13日 2:11
    2013年12月13日 2:08