65.9K
CodeProject 正在变化。 阅读更多。
Home

DelegateScheduler 类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.06/5 (7投票s)

2006年10月26日

7分钟阅读

viewsIcon

58167

downloadIcon

1292

一个 C# 类,允许你调度委托调用。

Sample Image - DelegateSchedulerDemo.png

引言

2005 年,我写了一个用于使用 .NET 创建状态机的工具包。在我关于状态机的文章的第二部分中,我使用了简单的交通信号灯作为示例,演示了如何实现分层状态机。我最初的实现方式在处理定时器事件以指示灯光变化方面有些天真。幸运的是,来自"leeloo999"帖子非常有帮助,它指向了我一种更好的方法。该解决方案涉及使用一个事件队列,该队列提供了排队定时事件的功能。我记下了 leeloo999 的建议,但直到我编写一个复杂得多的状态机时,他的建议才真正对我来说有意义。

leeloo999 建议我查看的事件队列看起来非常有前途。但是,我很少能抵挡住“自己动手”的诱惑,所以我决定写一个自己的版本;我想让我的类与我的DelegateQueue类相辅相成。这个新类将命名为DelegateScheduler

但是,在能写它之前,我需要一个优先级队列来按排序顺序存储定时事件。这促使我探索改编跳表作为优先级队列。最终,我写了我的PriorityQueue类。有了这个类,我就可以开始实现我的DelegateScheduler类了。

什么是委托调度器?

很多时候我们需要在特定时间间隔内发生事件。通常,一个简单的定时器就能解决问题。我们创建一个定时器,将其设置为以特定速率“滴答”,然后启动它。当定时事件发生时,我们通过执行任务来响应。还有其他情况,我们的定时要求要复杂得多。例如,我们可能需要不止一个定时事件发生,每个事件都在不同的时间间隔发生。此外,我们可能希望一个定时事件发生特定次数,而不是无限次。你可以用简单的定时器来实现这些要求,但这需要大量的设置。最好有一个类来为我们处理这个问题。DelegateScheduler就应运而生了:这个类提供了实现经典定时事件队列的功能。它允许你调度委托,在特定的时间间隔和特定的次数被调用。

实现

DelegateScheduler使用优先级队列来按调用顺序存储委托。当委托被添加到DelegateScheduler时,它会附带将在调用时传递给它的参数、调用次数以及决定调用频率的毫秒间隔。所有这些都存储在Task对象中,并放入DelegateScheduler的优先级队列中。

任务类

Task类是DelegateScheduler类的私有类。它代表一个计划事件。具体来说,它代表一个委托、它的参数,以及何时以及调用多少次该委托。它实现了IComparable接口,以便优先级队列可以确定Task的存储顺序。这是TaskCompareTo实现。

public int CompareTo(object obj)
{
    Task t = obj as Task;

    if(t == null)
    {
        throw new ArgumentException("obj is not the" + 
                  " same type as this instance.");
    }

    return -nextTimeout.CompareTo(t.nextTimeout);
}

Task类使用.NET Framework的DateTime类来表示下一次超时发生的时间,即它代表的委托应该被调用的时间。事实上,TaskCompareTo方法将其比较委托给它的DateTime对象:nextTimeout。但是,如果你仔细观察,你会发现一个负号反转了比较结果。

return -nextTimeout.CompareTo(t.nextTimeout);

考虑CompareTo方法应该如何工作。

  • 如果此实例小于 obj,则返回小于零的值。
  • 如果此实例等于 obj,则返回零。
  • 如果此实例大于 obj,则返回大于零的值。

优先级队列按降序存储项目。如果项目 A 的值大于项目 B,则项目 A 存储在项目 B 之前。也就是说,如果项目 A 的优先级值大于项目 B,则 A 放置在队列中的 B 之前(某些优先级队列提供更改此行为的功能)。对于Task对象,我们希望它们按升序放置在优先级队列中;如果Task A 的下一次超时发生在Task B 之前,则 A 放置在队列中的 B 之前。出于这个原因,有必要反转DateTime比较的结果。

轮询优先级队列

DelegateScheduler将定期轮询其优先级队列,以检查队列头部中的Task。如果其下一次超时的值已过,DelegateScheduler将将其出队,并指示它调用其委托。如果Task的计数设置为有限值,它将在调用其委托后递减其计数。DelegateScheduler检查Task的计数,如果计数大于零或计数表示无限计数,则将其放回优先级队列。

在调用其委托后,Task将更新其下一次超时值。它通过将创建时给它的超时值添加到它调用其委托的时间来实现这一点。有可能下一次超时已经过去了。如果超时设置为小于DelegateScheduler轮询其优先级队列的速率的值,则可能会发生这种情况。在这种情况下,DelegateScheduler将继续指示Task调用其委托,直到其计数达到零或下一次超时尚未过期。

这是DelegateScheduler轮询其优先级队列的代码。

lock(queue.SyncRoot)
{
    #region Guard

    if(queue.Count == 0)
    {
        return;
    }

    #endregion

    // Take a look at the first task in the queue to see if it's
    // time to run it.
    Task tk = (Task)queue.Peek();

    // The return value from the delegate that will be invoked.
    object returnValue;

    // While there are still tasks in the queue and it is time 
    // to run one or more of them.
    while(queue.Count > 0 && tk.NextTimeout <= e.SignalTime)
    {
        // Remove task from queue.
        queue.Dequeue();

        // While it's time for the task to run.
        while((tk.Count == Infinite || tk.Count > 0) && 
            tk.NextTimeout <= e.SignalTime)
        {
            try
            {
                Debug.WriteLine("Invoking delegate.");
                Debug.WriteLine("Next timeout: " + tk.NextTimeout.ToString());

                // Invoke delegate.
                returnValue = tk.Invoke(e.SignalTime);

                OnInvokeCompleted(
                    new InvokeCompletedEventArgs(
                    tk.Method,
                    tk.GetArgs(),
                    returnValue,
                    null));
            }
            catch(Exception ex)
            {
                OnInvokeCompleted(
                    new InvokeCompletedEventArgs(
                    tk.Method,
                    tk.GetArgs(),
                    null,
                    ex));
            }
        }

        // If this task should run again.
        if(tk.Count == -1 || tk.Count > 0)
        {
            // Enqueue task back into priority queue.
            queue.Enqueue(tk);
        }

        // If there are still tasks in the queue.
        if(queue.Count > 0)
        {
            // Take a look at the next task to see if it is
            // time to run.
            tk = (Task)queue.Peek();
        }
    }
}

Task调用了它的委托之后,DelegateScheduler会引发它的InvokeCompleted事件。此事件附带一个InvokeCompletedEventArgs对象。此对象包含有关调用的信息,例如被调用的委托、其参数以及其返回值。如果抛出了异常,它也将包含在内。

DelegateScheduler使用System.Timers.Timer来处理定时事件。每次其定时器引发Elapsed事件时,DelegateScheduler会通过轮询其优先级队列来响应。你可以通过设置PollingInterval属性来设置轮询发生的速率。此属性从定时器的Interval属性获取其值。

重量级与轻量级

DelegateScheduler类是经典定时事件队列的轻量级版本。重量级方法将使每个任务在自己的线程中运行。当任务需要运行时,它会发出信号让任务执行其工作。这是我最初采取的方法。但是,我决定降低类的开销,所以我让所有Task都在同一个线程上调用它们的委托。这个线程是System.Timers.TimerElapsed事件发生的线程。由于所有委托都在同一个线程上调用,因此必须考虑确保每个委托都不会花费太长时间来完成其工作。如果同时有多个Task超时,则尤其如此。

DelegateScheduler类有一个SynchronizingObject属性,它代表一个ISynchronizeInvoke对象。DelegateScheduler使用此对象来封送委托调用和事件。具体来说,它将属性的值委托给System.Timers.TimerSynchronizingObject属性。这确保了定时器的Elapsed事件被封送到SyncrhonizingObject的线程。在 WindowsForm中使用DelegateScheduler时,将SynchronizingObject属性初始化为Form本身。委托调用和事件将被封送到Form的线程。

依赖项

本文的演示项目下载包含整个Sanford.Threading命名空间。这包括我的DelegateQueue类。这个命名空间很小,而且DelegateQueueDelegateScheduler使用了一些相同的类,所以我决定将它们放在一起。你应该知道,Sanford.Threading命名空间依赖于我的另一个命名空间Sanford.Collections。在最新的更新中,我包含了Sanford.Collections程序集的发布版本。解决方案中需要该程序集的项目已链接到它。我这样做是希望下载能够“开箱即用”。过去这一直是令人沮丧的来源,我希望我已经找到了一个可行的解决方案。但是,如果你发现你需要我的任何程序集,你可以在这里找到它们。

结论

我希望你觉得这篇文章有用且信息丰富,并且希望代码能在你以后需要时派上用场。我期待你的评论和建议。保重。

历史

  • 2006 年 10 月 26 日 - 完成第一个版本。
  • 2007 年 3 月 12 日 - 更新了文章和下载。
© . All rights reserved.