博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

我的CCR之旅(3):CCR任务调度的原理和策略

Posted on 2008-09-12 04:02  熊掌  阅读(2977)  评论(12编辑  收藏  举报

一、缘起 

    在上一篇中,我给出了一个简单的CCR应用的例子用来演示CCR不用用户创建管理线程和资源的特性,但是让我高兴的是:有位叫“腊八粥”的朋友,对我给出的程序,做了改进后,发现了2个很奇怪的现象:一个是顺序有时会有点乱,一个是有些数据被丢失了。虽然我在后面的回复里面做了简单解释,但是感觉还是没有将彻底。因为这里涉及到了一个CCR内一个很重要的知识点,也是CCR的组成部分之一:任务调度。 

    PS:这张可能会有些难懂,因为我本计划把CCR的任务处理策略放到后面来讲的,但是既然有人提到了这个问题,那我觉得顺其自然把这篇文提前贴出来应该是比较好的,大家若能把例子程序改变下策略选项,自己运行下,应该还是能够轻松搞懂的。 

    OK,那我们就先看看 CCR是如何做任务调度 和 CCR提供的四种任务调度策略,然后再来看看为什么例子程序会出现“腊八粥”说的2个奇怪现象。 

二、CCR的任务调度 

    查阅MSDN:http://msdn.microsoft.com/en-us/library/bb648756.aspx  

    可知:(下面引用了Ncindy翻译的部分内容,感谢ncindy的辛苦劳动)

【一】:当一个元素被投递到附加了接收器的port,port的实现中将会发生如下操作:

step1.   为投递进来的元素创建一个容器。容器的类型(IPortElement)允许CCR在不知道元素类型的情况下将元素排队并将元素赋值给Task实例。

step2.   容器被放入队列。

step3.   如果接收器列表不是null,并且其中有一个以上的接收器,port对象将会调用ReceiverTask.Evaluate方法来让接收器和它里面的仲裁器层次检测元素是否可以被使用,在这个例子中,Evaluate方法将会返回true,并使用收到的元素和用户的delegate作为参数创建一个Task<int>实例。

step4.   port使用调用Evaluate方法返回的Task对象作为参数调用taskQueue.Enqueue,注意,当一个接收器是第一次被激活,它会被关联到由Arbiter.Activate方法提供的DispatcherQueue实例。

    当上面的4步完成之后,生成的Task对象现在已经被调度逻辑分发(dealt)给了对应的DispatcherQueue。 

【二】:一旦一个元素被放入DispatcherQueue,接下来将会做如下操作:

step1.   DispatcherQueue向它所属的Dispatcher发信号,告诉Dispatcher一个新的任务可以被执行了。

step2.   Dispatcher通知一个或者多个TaskExecutionWorker类型对象。每个TaskExecutionWorker对象管理一个操作系统线程。它将线程设置到一种高效的休眠状态,直到Dispatcher发出信号通知有元素可以被调度时。

step3.   TaskExecutionWorker对象调用DispatcherQueue.Test方法从队列中获取一个任务。如果是可用的任务,TaskExecutionWorker对象则调用ITask.Execute。

step4.   Task.Execute方法调用关联在task对象上的delegate,并将一个或者多个关联在task上的参数传递进去。 

    总之:在CCR中,线程池处理的任务,是由DispatcherQueue产生的;而DispathcerQueue有是根据用户线程通过Port或PortSet提交给的数据 和 初始化时指定的委托来产生任务的。因此可知影响任务调度的地方有3处:

1、客户端提交数据的地方:Port/PortSet的Post方法;

2、DispatcherQueue产生任务的地方:ReceiverTask的Evaluate方法;

3、Dispacher内线程池处理任务的地方:Task执行关联delegate的Execute方法; 

    而CCR就是通过给上面三处加入调度机制来达到任务调度的负载均衡目的的。 

三、CCR的四种任务调度策略

using System;

namespace Microsoft.Ccr.Core
{
    
public enum TaskExecutionPolicy
    {
        Unconstrained 
= 0,
        ConstrainQueueDepthDiscardTasks 
= 1,
        ConstrainQueueDepthThrottleExecution 
= 2,
        ConstrainSchedulingRateDiscardTasks 
= 3,
        ConstrainSchedulingRateThrottleExecution 
= 4,
    }
}

这4中策略分别应用在一下场景: 

1、ConstrainQueueDepthDiscardTasks 按队列深度丢弃最旧任务

    适用于:要处理的消息可以丢弃但是必须保存最近N条的情况。这对于CPU处理速度低于消息产生速度的情况很有好处,该策略能够保证丢弃的最旧任务的同时最新的N个任务能都得到调度。特别是在阻塞深度为1的时候,队列中保存的始终都是最新的任务。 

2、ConstrainQueueDepthThrottleExecution 按照队列深度阻塞任务产生

    适用于:消息不是规律产生,而是随机、爆炸性到达的情况。这对于来自网络获知其他机器的消息很相似,该策略保证任务不会被丢失,通过阻塞消息Post到Port/PortSet的方法来降低任务产生的速度。 

3、ConstrainSchedulingRateDiscardTasks 按照固定速度处理消息且丢失未处理的最旧消息

    适用于:处理产生速度有规律的消息,比如播放视频。在这种情况下一般所有的消息已经不是最重要的了,但保存最新的消息却很有意义,该策略能够保证代码会以固定的速度执行,即使消息以爆炸式的速度产生也没关系。 

4、ConstrainSchedulingRateThrottleExecution 按照固定速度处理消息且阻塞任务缠上

    适用于:消息产生源是同一处理器中的另一线程时。该策略会让消息的产生源慢下来,适应消息的处理速度,保证不会有任务丢失。 

四、问题&解答 

问题 例子程序在运行的时候出现:数据会乱序 和 数据丢失的现象。(下面是例子程序的主要代码,与上一篇代码完全相同,此处贴出,是为了阅读方便) 

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading;
 6 using Microsoft.Ccr.Core;
 7 
 8 namespace CCRDemo1
 9 {
10     class Program
11     {
12         static void Main(string[] args)
13         {
14             int maxiQueueDepth = 10;
15             // step1: 创建一个Dispatcher对象
16             Dispatcher dispatcher = new Dispatcher(0"调度器名称");
17             // step2: 创建一个与step1创建对象关联的DispatcherQueue对象
18             DispatcherQueue depthThrottledQueue = new DispatcherQueue(
19                                             "任务队列的名称",
20                                             // 关联到该队列的调度器
21                                             dispatcher,
22                                             // 队列保存数据的策略:保存最近消息策略
23                                             TaskExecutionPolicy.ConstrainQueueDepthDiscardTasks,
24                                             // 队列的深度
25                                             maxiQueueDepth
26                                             );
27             // step3: 创建一个能够接收整型数据的Port
28             Port<int> intPort = new Port<int>();
29             // step4: 把Port与处理函数关联,然后再与DispatcherQueue关联
30             Arbiter.Activate(depthThrottledQueue,
31                             Arbiter.Receive(true,
32                                         intPort,
33                                         delegate(int i)    // 这里用了一个匿名方法,作为处理函数
34                                             {
35                                                 Thread.Sleep(2000);
36                                                 Console.WriteLine("[{0}] {1}", DateTime.Now.ToString("o"), i);
37                                             }
38                                         )
39                             );
40 
41             // step5: 快速的提交大量的任务
42             Console.WriteLine("[{0}] 开始提交大量的任务", DateTime.Now.ToString("o"));
43             for (int i = 0; i < maxiQueueDepth * 100000; i++)
44             {
45                 // 把数据Post到intPort内
46                 intPort.Post(i);
47             }
48             Console.WriteLine("[{0}] 大量任务提交完毕。", DateTime.Now.ToString("o"));
49 
50             Console.WriteLine("Press any key to exit");
51             Console.ReadKey();
52             dispatcher.Dispose();
53         }
54     }
55 }
56 

 

解答

1、数据乱序问题

    CCR内部创建了多线程池来执行这个匿名方法,而且执行的方式是并发、异步,因此改匿名方法打印出来的数字的顺序自然就应该是不可预知的,也就是说:顺序是乱的;

2、数据丢失问题

    这个也是正常的,例子代码在瞬间提交的大量数据,提交的速度,远远超过匿名方法处理的速度(里面sleep了2秒),因此这意味着会有大量的任务堆积在DispatcherQueue内,然而,改队列在创建的时候,已经指明了任务调度策略为:TaskExecutionPolicy.ConstrainQueueDepthDiscardTasks,因此DispatcherQueue内只会保存最新的任务,旧的就会丢失。

 

五、附录 

1、示范代码:下载

2、本系列其他文章:

我的CCR之旅(1):来自微软机器人技术的新并发、异步解决方案

我的CCR之旅(2):打开CCR的大门,编写一个不用创建线程,不用考虑资源互斥的多线程程序