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 日 - 更新了文章和下载。