从工作线程调用 UI 的另一种方式






4.75/5 (29投票s)
2005年10月3日
6分钟阅读

319292

2249
本文演示了一种从工作线程调用 UI 事件处理程序的替代方法。
引言
本文演示了一种从工作线程调用 UI 事件处理程序的替代方法。
来自工作线程的事件 - “传统”方式
.NET 平台的一大优势在于,它提供了一种更简单的方式来执行耗时任务,同时保持用户界面的响应性。您可以创建一个对象,用适当的数据填充其属性和字段,并将对象的一个方法作为后台线程运行。然后,稍后检索其工作结果。
当后台线程(也称为工作线程)需要显示某些 UI 信息时,您只需定义一个事件,让 UI 对象订阅该事件,然后在后台线程中触发它。唯一的问题是,UI 应该由包含消息循环的自身线程来管理。微软为我们提供了一个简单的变通方法。
假设我们有如下所示的事件处理程序:
void OnEvent(object sender, EventArgs e)
{
// Update the UI
}
它适用于任何派生自 System.Windows.Forms.Control
的对象。为了使其线程安全,您需要添加一些代码:
void OnEvent(object sender, EventArgs e)
{
if(InvokeRequired)
Invoke(new EventHandler(OnEvent),
new object[] {sender, e});
else
{
// Update the UI
}
}
Control
类中的 InvokeRequired
属性会在 UI 元素的成员函数从不同的线程调用时返回 true
。在这种情况下,我们应该使用 Invoke
方法进行线程间封送。
使 Windows Forms 程序支持多线程的“传统”方式是不错的,但想象一下,如果您有一个相当健谈的工作对象,它有很多事件。那么,对于每个事件,您都需要修改事件处理程序。我通常会忘记这样做,然后去查找 bug。我也认为这种变通代码很丑陋,应该隐藏在工作对象的客户端之外。我们应该将它移到工作对象的成员函数中,我将向您展示如何做到这一点。
来自工作线程的事件,一种更简单的方法
微软采用了一种独特的引发事件的模式。名为 AAA
的每个事件都附带一个 protected
方法 OnAAA
,该方法会引发事件,并且可以被派生类重写。在程序中使用这种模式有很多原因,我将为您提供另一个原因。检查我们是否正在从非 UI 线程调用 UI 应该在这种方法中进行。
您可能会问如何实现?很简单。Control
类实际上实现了 System.ComponwentModel.ISynchronizeInvoke
接口。该接口声明了 InvokeRequired
属性以及 Invoke
、BeginInvoke
和 EndInvoke
方法,因此,理论上存在更多了解消息循环的类。在引发事件时,我们需要检查每个目标对象是否实现了此接口。如果实现了,我们需要检查 InvokeRequired
属性,而不是直接调用事件委托,我们需要使用 Invoke
方法。这意味着对于事件订阅者(大多数情况下是 Form
派生类),此事件将始终是同步的,并且订阅者的作者无需担心线程间封送。
但是,我们应该记住,所有事件都是多播委托。因此,我们必须单独检查所有事件订阅对象。这很简单,因为 System.MulticastDelegate
类有一个 GetInvocationList
方法,该方法返回一个表示合并的多播委托的单播委托数组。
假设我们声明了一个事件:
public event EventHandler Event;
我们应该这样声明配套的方法:
protected virtual void OnEvent(EventArgs e)
{
EventHandler handler = Event;
if(null != handler)
{
foreach(EventHandler singleCast in handler.GetInvocationList())
{
ISynchronizeInvoke syncInvoke =
singleCast.Target as ISynchronizeInvoke;
try
{
if((null != syncInvoke) && (syncInvoke.InvokeRequired))
syncInvoke.Invoke(singleCast,
new object[] {this, e});
else
singleCast(this, e);
}
catch
{}
}
}
}
方法的第一行,赋值语句,使方法线程安全。我获取事件处理程序的本地副本,并确保即使有人修改了 Event
,也不会出现任何问题。然后,我检查是否有任何事件订阅者。如果有订阅者,我检查事件的委托目标对象是否实现了 ISynchronizeInvoke
接口。如果实现了,并且对象位于 UI 线程中,我将执行线程间封送。在所有其他情况下,我直接调用委托。我还捕获了订阅者可能抛出的所有异常。忽略它们不是一个好主意,但我还没有找到一种好的方法将它们传递给 OnAAA
方法的调用者。
示例
在示例中,您会找到一个名为 Copier
的简单组件,它在后台线程中将一个流复制到另一个流。如果您需要复制大文件或从 Internet 下载数据,您可能需要一个这样的类。Copier
类具有一些与工作进度相关的属性,三个线程安全的事件 Started
、Progress
和 Finished
,以及三个 public
方法 Start
、Stop
和 Join
,它们会检查 Copier
的状态,如果有效,则将工作委托给 System.Threading.Thread
方法。
示例还包含 ProgressForm
类,它为 Copier
组件提供了一个简单的 UI,以及 MainForm
类,它允许用户指定源文件和目标文件的名称。请注意,Copier
不关闭数据流;这是 Copier
客户端的责任。ProgressForm
类也是如此。
此代码中有两个值得关注的地方。第一个是 OnStarted
、OnProgress
和 OnFinished
方法的实现,它们遵循所描述的模式。第二个是取消后台线程的能力。最初,我从 CancelEventArgs
派生了 ProgressEventArgs
类,但是 Control.Invoke
调用不会将其参数封送回调用线程。我已将 Stop
方法添加到 Copier
类中,该方法可以优雅地停止后台线程。
我使用 SharpDevelop IDE 和 NAnt 0.85 作为构建系统编写了此示例。使用 NAnt,您应该调用 Debug、Release 或 Doc 目标;后者会在 doc 文件夹中创建一个 InvokeUI.chm HTML 帮助文件。您还可以使用 build.bat 文件来编译示例。抱歉,我没有 Visual Studio,而拥有 Visual Studio 的用户将不得不重新创建项目。只需创建一个空的 Windows Forms 项目,然后将所有 *.cs 文件添加到其中。
许可证
文件 Copier.cs、ProfressEvent.cs 和 ProgressForm.cs 受 BSD 风格许可证的保护,请参见每个文件开头的注释。其余代码属于公共领域。使用示例风险自负。
链接
- 在此处,您可以了解 SharpDevelop 并下载您的副本。
- 在此处,您可以了解 NAnt 并下载您的副本。我个人推荐这个工具。
- MSDN 文章:“定义事件” - 描述了
Event
/OnEvent
模式,并展示了如何使用System.ComponentModel.EventHandlerList
类优化事件实现。 - Chris Sells 的文章:“安全、简单的 Windows Forms 多线程” - 对 Windows Forms 中的多线程 UI 如何工作进行了简单清晰的解释。
- Patrick Cauldwell 的一篇博文 描述了
InvokeRequired
属性的一个问题。
修订历史
- 2005/09/03:初始发布
- 2005/09/04:修正了后台线程取消的一个严重 bug;修正了
ProgressForm
标签问题;将 HtmlHelp 编译添加到构建文件中
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。