DelegateQueue 类






4.81/5 (43投票s)
ISynchronizeInvoke 接口的实现。
目录
- 引言
- ISynchronizeInvoke 接口
- 实现 ISynchronizeInvoke 接口
- 异常处理
- 派生自 SynchronizationContext 类
- DelegateQueue 类概述
- 依赖项
- 结论
引言
去年,我编写了一个用于创建状态机的 工具包。该工具包的目标之一是提供每个状态机在自己的线程中运行的选项;我希望状态机成为活动对象。为了实现这个目标,我需要一个事件队列。状态机会使用事件队列将发送给它们的消息入队,然后在它们自己的线程中出队。经过几次设计,我最终创建了一个 DelegateQueue 类。这个类提供了将委托及其参数放入队列,然后在专用于调用它们的线程中出队的功能。每个状态机都使用一个 DelegateQueue 对象来实现异步行为。
此外,我认为让 DelegateQueue 类实现 ISynchronizeInvoke 接口会很好。随着 .NET v2.0 的出现,我希望 DelegateQueue 类能够派生自新的 SynchronizationContext 类。它不仅仅是一个对我的状态机工具包有用的类,它本身就是一个有用的类。因此,我已将其从我的状态机工具包中移除,并将其放入了我创建的新命名空间 Sanford.Threading 中。
接下来将描述 ISynchronizeInvoke 接口,以及我的 DelegateQueue 类的实现。我还将介绍如何重写 SynchronizationContext 类中的方法。
ISynchronizeInvoke 接口
ISynchronizeInvoke 接口代表用于同步或异步调用委托的功能。不幸的是,.NET Framework 中实现此接口的类很少。实际上,只有 Control 类及其派生类。我们大多数人都熟悉禁止在创建它的线程以外的任何线程上修改或访问 Control 的规定。ISynchronizeInvoke 接口代表一组可以从任何线程访问的方法和属性。它提供了将操作封送到 ISynchronizeInvoke 对象正在运行的同一线程的能力。封送操作可确保它以线程安全的方式执行。在检查了 ISynchronizeInvoke 接口的成员后,我将提供一个示例。
让我们看看 ISynchronizeInvoke 接口成员
- 方法- BeginInvoke
- EndInvoke
- Invoke
 
- 属性- InvokeRequired
 
BeginInvoke、EndInvoke 和 Invoke 方法
除了返回值,BeginInvoke 和 Invoke 方法具有相同的签名
IAsyncResult BeginInvoke(Delegate method, object[] args);
object Invoke(Delegate method, object[] args);
第一个参数是代表要调用的方法的委托。第二个参数是对象数组,代表调用委托时要传递给委托的参数。这两个方法的作用相同,它们会在 ISynchronizeInvoke 对象正在运行的线程上执行传递给它们的委托。但是,BeginInvoke 异步操作,而 Invoke 同步操作。调用 BeginInvoke 时,它会立即返回,而不会等待指定的委托被调用。相比之下,Invoke 直到指定的委托被调用后才会返回。
BeginInvoke 方法返回一个 IAsyncResult 对象,代表 BeginInvoke 操作的状态。客户端可以将 IAsyncResult 对象传递给 EndInvoke 方法,以等待委托被调用。EndInvoke 和 Invoke 方法都返回一个对象,代表委托调用的返回值。这意味着如果委托返回 null 值或其返回类型为 void,则返回值将为 null。
InvokeRequired 属性
InvokeRequired 属性代表一个 bool 值,指示是否必须调用 BeginInvoke 或 Invoke 才能在 ISynchronizeInvoke 对象上调用操作。如果 InvokeRequired 在 ISynchronizeInvoke 对象正在运行的线程以外的线程上进行检查,则其值为 true;否则,其值为 false。
例如,假设一个 Windows Form(它派生自 Control 类,因此实现了 ISynchronizeInvoke 接口)收到一个对象发来的事件,告诉它更新自身或其某个控件。在其事件处理程序中,它会检查其 InvokeRequired 属性,看其值是否为 true。如果是,则需要将一个代表实际事件处理逻辑的方法的委托传递给 BeginInvoke 或 Invoke。当委托被实际调用时,它将被封送到与 Form 运行在同一线程上。否则,如果 InvokeRequired 属性为 false,则可以直接调用事件处理方法。
private void SomeEventHandler(object sender, EventArgs e)
{
    if(InvokeRequired)
    {
        EventHandler handler = new EventHandler(UpdateControl);
        BeginInvoke(handler, e);
    }
    else
    {
        UpdateControl(sender, e);
    }
}
private void UpdateControl(object sender, EventArgs e)
{
    someLabel.Text = "Some event occurred.";
}
Begin Invoke - 细节
我注意到 Windows Form 实现 BeginInvoke 方法时的一个有趣之处。如果 BeginInvoke 在 Form 运行的同一线程上被调用,Form 不会等待调用方法;它会同步调用它。例如,考虑这个 Form 类
namespace FormTest
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        private void button1_Click(object sender, EventArgs e)
        {
            BeginInvoke(new MethodInvoker(delegate ()
            {
                MessageBox.Show("Hello, ");
            }));
            MessageBox.Show("World!");
        }
    }
}
第一个 MessageBox 显示“Hello, ”,然后第二个 MessageBox 显示“World!”。换句话说,似乎如果 BeginInvoke 在 Form 运行的同一线程上被调用,它会同步调用传递给它的方法。当我们考虑这一点时,这是有道理的。如果上面的代码调用了 EndInvoke 来等待调用完成并且调用是异步进行的,那么我们将发生死锁。Form 的线程在有机会调用方法之前就会阻塞。
如果我们更改代码以检查 IAsyncResult 对象以查看操作是否同步完成,结果是 false。坦白说,这让我很困惑。
IAsyncResult result = BeginInvoke(new MethodInvoker(delegate ()
{
    MessageBox.Show("Hello, ");
}));            
MessageBox.Show("World!");
EndInvoke(result);
MessageBox.Show(result.CompletedSynchronously.ToString());
第三个消息框显示 false。
我不想在此问题上离题,但我仍然不理解为什么 CompletedSynchronously 属性不是 true。这没有意义。
实现 ISynchronizeInvoke 接口
实现 ISynchronizeInvoke 接口很简单,但有一些灰色区域,我将对此进行描述。我还将描述 DelegateQueue 类的整体设计,并逐一介绍 ISynchronizeInvoke 接口的方法和属性,并讨论 DelegateQueue 类如何实现它们。
DelegateQueue 类在其整个生命周期中都在一个线程中运行。每次调用 BeginInvoke 或 Invoke 时,指定的委托及其参数都会被放入一个队列。DelegateQueue 正在运行的线程会被信号通知,它会从队列头部出队委托并调用它。通过这种方式,DelegateQueue 类使用了无界缓冲区架构。出队委托并调用它们的过程一直持续到 DelegateQueue 被释放为止。届时,线程将结束。
由于 DelegateQueue 使用无界缓冲区,存在溢出的危险。只要 DelegateQueue 可以以与入队速度大致相同的速度出队和调用委托,这就不应该成为问题。这只是需要注意的一点。
实现 BeginInvoke、EndInvoke 和 Invoke
调用 BeginInvoke 方法时,它会创建一个 DelegateQueueAsyncResult 对象。DelegateQueueAsyncResult 类是一个 private 类,它实现了 IAsyncResult 接口,并提供了 DelegateQueue 类使用的附加功能。在创建时,该对象被传递了指定的委托及其参数。然后将其放入队列,并向线程发出信号,表明它有一个委托需要调用。最后,BeginInvoke 方法将 DelegateQueueAsyncResult 对象返回给客户端。由于 BeginInvoke 方法的返回类型是 IAsyncResult 对象,客户端只能看到 IAsyncResult 接口公开的方法和属性。如果客户端希望等待委托调用的结果,可以调用 EndInvoke,并将 IAsyncResult 对象传递给它。EndInvoke 将阻塞直到委托被调用并返回。
IAsyncResult 接口有一个类型为 WaitHandle 的 AsyncWaitHandle 属性。当 IAsyncResult 对象传递给 EndInvoke 方法时,该方法使用此属性等待来自 DelegateQueue 线程的信号,表明它已调用委托。DelegateQueueAsyncResult 类使用 ManualResetEvent 对象而不是 AutoResetEvent 对象来实现 AsyncWaitHandle 属性。原因是,在调用 EndInvoke 之前,委托可能已被调用并发出信号。使用 ManualResetEvent 而不是 AutoResetEvent 可确保在调用 EndInvoke 检查事件之前,事件将保持已发出信号状态而不重置。
考虑到上面描述的关于 BeginInvoke 在 Windows Form 中的行为的观察结果,我已将 DelegateQueue 更改为类似的行为。当 BeginInvoke 在 DelegateQueue 运行的同一线程上被调用时,它会立即调用指定的委托,而不会将其入队。
实现 Invoke 只是将委托及其参数入队,然后返回 EndInvoke 的结果。
public object Invoke(Delegate method, object[] args)
{
    if(InvokeRequired)
    {
        DelegateQueueAsyncResult result = new DelegateQueueAsyncResult(this, 
                method, args, false, NotificationType.None);
        lock(lockObject)
        {
            delegateDeque.PushBack(result);
            Monitor.Pulse(lockObject);
        }
        returnValue = EndInvoke(result);
    }
    else
    {
        // Invoke the method here rather than placing it in the queue.
        returnValue = method.DynamicInvoke(args);
    }
    return returnValue;
}
Invoke 将阻塞直到 EndInvoke 返回,从而实现同步行为。如果 Invoke 在 DelegateQueue 运行的同一线程上被调用,它会立即调用指定的委托。
除了 Invoke 和 BeginInvoke 方法之外,DelegateQueue 类还提供了这两个方法不在 ISynchronizeInvoke 接口中的两个变体,称为 InvokePriority 和 BeginInvokePriority。这两个方法与其对应的功能相同,但允许您将委托放置在队列的开头而不是末尾。这使得委托的调用比队列中已有的委托具有更高的优先级。
在调用 BeginInvoke 或 BeginInvokePriority 以调用委托后,DelegateQueue 会引发一个名为 InvokeCompleted 的事件。伴随此事件的 EventArgs 派生对象称为 InvokeCompletedEventArgs。它包含有关调用的信息,例如被调用的方法、其参数以及委托的返回值。此外,如果委托在调用时抛出了异常,Error 属性将表示该异常。
InvokeCompleted 事件也不是 ISynchronizeInvoke 接口的一部分。但是,我认为在委托调用后接收事件通知以及有关调用的信息会很有帮助。如果发生异常,这可能特别有用。
实现 InvokeRequired
InvokeRequired 属性通过比较 DelegateQueue 的工作线程 ID 与当前线程(正在检查 InvokeRequired 属性的线程)的 ID 来实现。InvokeRequired 属性的代码很简单
public bool InvokeRequired
{
    get
    {
        return Thread.CurrentThread.ManagedThreadId != 
                                        delegateThread.ManagedThreadId;
    }
}
异常处理
当被调用的委托抛出异常时,该怎么办?ISynchronizeInvoke 的 Invoke 方法的文档如下说明
调用过程中抛出的异常会传播回调用者。
可以。DelegateQueue 通过捕获被调用委托抛出的任何异常,并从 Invoke 方法(实际上是从 Invoke 方法内部调用的 EndInvoke 方法)重新抛出来实现这一点。但是,当调用 BeginInvoke 而不是 Invoke 时会发生什么?在这里,文档不那么清楚。通过测试 Windows Form,我发现异常会从 EndInvoke 方法中重新抛出。但是,不能保证客户端会调用 EndInvoke 方法。因此,Windows Form 类也会从发生异常的地方重新抛出异常。应用程序将其视为未处理的异常。
从发生异常的地方重新抛出异常不是 DelegateQueue 类的选项。这会终止 DelegateQueue 的线程;这不是我们想要的。相反,当由于调用 BeginInvoke 或 BeginInvokePriority 而调用委托时,会捕获该异常并在 EndInvoke 中重新抛出,正如 Windows Control 类一样。此外,异常会通过 InvokeCompleted 事件传递。
派生自 SynchronizationContext 类
SynchronizationContext 类是一个新的 .NET Framework 类,代表一个同步上下文。它的目的与 ISynchronizeInvoke 接口相似,因为它代表了一种将委托调用从一个线程封送到另一个线程的方法。SynchronizationContext 类比 ISynchronizeInvoke 接口具有一个优势,那就是它有一个静态的 Current 属性,可以让你访问当前线程的 SynchronizationContext 对象。这使得将封送事件的责任从接收者转移到发送者更加容易。
我认为从 SynchronizationContext 类派生 DelegateQueue 类会很有用。具体来说,我想重写 SynchronizationContext 类的 Post 和 Send 方法。我还希望每个 DelegateQueue 对象将自身设置为其所代表线程的 SynchronizationContext 对象。这一切都很容易做到。以下是 Send 和 Post 方法的实现
public override void Send(SendOrPostCallback d, object state)
{
    Invoke(d, state);
}
Send 方法代表用于同步将消息发送到 SynchronizationContext 的功能。为了在 DelegateQueue 类中实现这一点,我只是将 Send 的调用委托给 Invoke 方法。
最初,我让 Post 方法直接将调用委托给 BeginInvoke 方法。但是,由于我已经改变了 BeginInvoke 的行为,如上所述,我已经更新了 Post 的实现
public override void Post(SendOrPostCallback d, object state)
{
    lock(lockObject)
    {
        delegateDeque.PushBack(new DelegateQueueAsyncResult(this, d, 
             new object[] { state }, false, NotificationType.PostCompleted));
        Monitor.Pulse(lockObject);
    }
}
Post 方法异步调用指定的无关它是否在 DelegateQueue 运行的同一线程上被调用。Post 方法没有 EndInvoke 对应的功能,因此没有死锁的危险。我特别希望每个 Post 调用都异步执行,以便与我的状态机工具包一起使用。
有一个称为“运行至完成”(RTC)的概念。应用于状态机,它的意思是每个转换都必须在触发另一个转换之前完成。如果不强制执行此操作,状态机可能会处于未定义状态。我不会在这里深入讨论细节,但底线是,如果我的 Post 方法表现得像 BeginInvoke,当状态机发送消息给自己(使用 DelegateQueue 的 Post 方法)时,RTC 将会被违反。
DelegateQueue 在其线程方法内部,通过一行代码将自身设置为其线程的 SynchronizationContext
// Set this DelegateQueue as the SynchronizationContext for this thread.
SynchronizationContext.SetSynchronizationContext(this);
这使得只要属性是从 DelegateQueue 线程上的某个地方访问的,就可以通过 SynchronizationContext 的静态 Current 属性访问 DelegateQueue。
与 BeginInvoke 和 BeginInvokePriority 方法一样,在调用 Post 方法调用委托后,会引发一个事件。该事件 PostCompleted 携带一个 EventArgs 派生类 PostCompletedEventArgs。此类表示有关事件的信息,例如被调用的回调方法、与回调一起传递的状态对象以及 Error 属性,表示回调调用时可能抛出的异常。
DelegateQueue 类概述
既然我们已经介绍了 DelegateQueue 类部分内容的实现方式,我认为列出属于 DelegateQueue 类的方法和属性可能会很有用。
- ISynchronizeInvoke方法- BeginInvoke
- EndInvoke
- Invoke
 
- ISynchronizeInvoke属性- InvokeRequired
 
- SynchronizationContext方法重写- Post
- 发送
 
- DelegateQueue方法- InvokePriority
- BeginInvokePriority
- SendPriority
- PostPriority
 
- DelegateQueue事件- InvokeCompleted
- PostCompleted
 
此外,DelegateQueue 类实现了 IComponent 和 IDisposable 接口。
依赖项
本文的演示项目下载包含整个 Sanford.Threading 命名空间。这包括我的 DelegateScheduler 类。这个命名空间很小,DelegateQueue 和 DelegateScheduler 使用了一些相同的类,所以我决定将它们放在一起。但是,您应该知道,Sanford.Threading 命名空间依赖于我的另一个命名空间 Sanford.Collections。在最新的更新中,我包含了我 Sanford.Collections 程序集的发布版本。解决方案中需要该程序集的项目已链接到它。我这样做是希望下载能够“开箱即用”地编译。这过去一直令人沮丧,我希望我终于找到了一个可行的解决方案。但是,如果您发现需要我的任何程序集,可以 在这里 获取。
结论
我希望您发现这篇文章有趣且有用。编写这个类是一次有趣的经历,也很有回报。评论和建议一如既往地受到欢迎。保重。
历史
- 2005 年 10 月 26 日 - 第一个版本。
- 2006 年 1 月 3 日 - 切换到无界缓冲区。
- 2006 年 6 月 21 日 - 从 SynchronizationContext派生类,并添加了InvokeCompleted事件。文章重大更新。
- 2006 年 10 月 17 日 - 更新了文章和代码。
- 2007 年 3 月 12 日 - 更新了文章和下载。


