闲话时间调度算法

最近一直在忙于做一个分布式的作业调度器。与通常的作业调度器不同,整个系统中没有调度中心的,所有入网的服务器都通过公共的协议协商工作。N年前的一个同事目前在一家很有前途的公司发展,前些日子开发了一套用于本公司应用产品的公共平台,其中也包括一个作业调度引擎。前几天园子里也有博友发布了开源的作业调度器。可见,作业调度在企业应用集成一天天普及的今天具有重要意义。

作业调度其中有一个重要的细节是时间调度,当作业进入调度计划时,必须按指定的时间触发。如何最大效率地、尽可能精准地、尽可能消耗最少资源地完成这个时间调度,还是很有些微妙的。其实功能非常简单:组织一个作业列表,按每个作业项指定的时间触发这个作业。例如有一组作业,其中最近的一个作业在10分钟后将被触发,在等待期间,又一个新的作业被加入队列,且应该在5分钟后将被触发。通常是立即撤销前一个计划,换成新的计划。所以,这个时间调度一定要能够撤销某项计划,适应动态变化的需要。

WM_TIMER

定时消息是一个选择,唯一依赖的是一个消息循环。可惜服务器通常不依赖消息循环来工作。虽然可以创建一个窗口或线程并建立消息循环,但是很显然,这是一个串行化的过程,有悖于服务器通常所要求的可伸缩性。

Sleep函数或其它等候函数的超时

睡眠函数就是让CPU沉睡一段时间。沉睡多久?上个世纪的某个时候我真的这么用过,用MsgWaitForMultipleObjects函数来处理延时。为了防止超时值被不断插入的处理所破坏,我采用逐步逼近法,就是每次等候函数的超时值为需要延时的时间的一半多一点儿,在触发时再进行调整,直到满足时间精度要求时触发作业。

WaitableTimer

可等待的定时器是一个优于定时器消息的选择,毕竟这是一个内核对象,控制自如,可用在任何线程中。虽然WaitableTimer也是跨进程的,不过我建议还是不要这么前卫了,毕竟你无法访问另外一个进程的内存。

算法

一个作业项配备一个WaitableTimer?这样可以很轻松地令系统崩溃,因而这种想法很对对Windows有仇恨的程序员的味口。所以,用单个WaitableTimer再配以一个线程安全的列表是非常合适的。当有新的作业加入时,立即取消从前的定时,重排作业表,将需要最先被触发的作业挑出来,如果已经超时,则立即触发并让别的线程有机会执行这个作业,然后再挑下一个需要最先被触发的作业,如此往复下去直到再挑不出需要立即触发的作业为止;如果发现需要等待一段时间再触发的作业,就按该作业的要求来重新设定WaitableTimer;如果没有了需要触发的作业,也将WaitableTimer的时间间隔设置为第二天的同一时刻。当WaitableTimer被触发时,所需要做的仅仅是同一件事情,继续挑出需要立即触发的作业,然后如此地往复下去。

最后需要考虑的事情就是效率了。时间值是一个比较大的结构,可以采用64位的负数或者正的浮点数,前者为相对时间,后者为绝对时间,当作业项多的时候,即使是采用二分法还是需要花费比较多的CPU时间去比较。其实我并不是单纯为了节省CPU时间,而是担心因为耗费了额外的CPU时间而导致作业被延误。有更快的算法么?将时间值用两个无符号的32位整数来表示,比较的时间仅仅只需要做两次寄存器的减法。第一个无符号的整数是当前的TickCount,以毫秒为单位。当然,这个很容易溢出,因为一个32位整数只能表示4294967295毫秒,也就是49.7个自然天。所以就需要另外一个32位整数了,这个整数仅仅用来记录轮次(你已经知道了,每个轮次表示49.7)。这个轮次很容易调整。如果目前取出来的TickCount比上一个TickCount小则说明计数器已经复位过,已经进入一个新的轮次。需要两个额外的32位无符号整数来记录最后的校准值。所以,作业项的时间刻度需要一个结构来记录,该结构记录作业进入时的TickCount和轮次,再记录需要延迟的毫秒数。这样,所记录的信息与当前TickCount和轮次进行比较很容易明确作业是否已经超时。采用TickCount的另外一个意义是这个API效率高于GetSystemTime,别小看这一点差异,因为这几个API调度频度极高,差异就积少成多了。现在是否已经明白在发现作业列表中没有任何作业的时候为什么还要设置定时器了么?是的,仅仅只是重新校准一下而已。当然,我承认设置1天和49天是没有什么太大的差异的。但是,设置50天就有本质差异了,没准你就有中六合彩的彩头,刚好在这个空档,TickCounter复位了!获得这么好运气的机率是:

(50*24*60*60*1000-4294967296)/ 4294967296=5.8

虽然很小,但的确大于0。

posted @ 2007-08-29 14:16  双鱼座  阅读(3667)  评论(7编辑  收藏  举报