用 C# 编写多线程 GUI 的另一种方法






1.76/5 (36投票s)
一篇关于编写响应式多线程 GUI 的文章,但不是微软的方式

引言
本文概述了一种用 C# 编写响应式多线程 Windows Forms GUI 的替代方法。当我说“替代”时,我的意思是采用一种不遵循当前微软教条的技术,即只有创建 GUI 控件的线程才能与其交互。这项技术仅在 GUI 中的一个或多个控件每秒处理数十或数百条消息,导致 GUI 变得无响应时才应考虑。这通常在使用实时数据源时是这样。有问题的控件应该是只读的,不通过 GUI 更新。在所有其他情况下,应使用标准的 BeginInvoke
/SycnchronizationContext
/AsyncOperation
调用。对于大多数 Windows Forms GUI,此技术不适用。
背景
很多年前,当我编写实时 C 语言 Windows 应用程序时,将应用程序的多个子窗口与多个线程关联起来以提高 GUI 的响应能力是很常见的。多年来,这项技术的使用(和文档记录)越来越少。最初,微软记录并鼓励开发人员使用这项技术。今天,这项技术已变得完全禁忌。现在,当我与其他 .NET 开发人员讨论这项技术时,他们甚至不相信它会起作用。通过本文,我旨在表明在某些情况下(请参阅引言),从其他线程更新 GUI 控件是完全可以(且安全)的。
示例代码
示例应用程序是一个非常基本的概念验证框架。它不包含任何验证或异常处理代码。显然,在生产代码中不会是这样。示例代码首先创建要与选定的主窗体的子控件关联的线程并使其运行。这是在主窗体的 Load
事件处理程序中完成的。
private void theMainForm_Load(object sender, EventArgs e)
{
// Prevent the framework from checking what thread the GUI is updated from.
theMainForm.CheckForIllegalCrossThreadCalls = false;
// Create our worker threads and name them.
// The name will be used to associate a thread with a specific ListView
worker1 = new Thread(new ThreadStart(UpdateListView));
worker1.Name = "Worker1";
worker2 = new Thread(new ThreadStart(UpdateListView));
worker2.Name = "Worker2";
worker3 = new Thread(new ThreadStart(UpdateListView));
worker3.Name = "Worker3";
worker4 = new Thread(new ThreadStart(UpdateListView));
worker4.Name = "Worker4";
// Get all the threads running.
Start();
}
请注意,在上面的代码中,我们将窗体的 CheckForIllegalCrossThreadCalls
属性设置为 false
。此属性是在 .NET 2.0 中添加的。如果未将其设置为 false
,框架将在运行时抛出 InvalidOperationException
,如下所示

由线程执行的 UpdateListView
方法只是反复填充然后清空其关联的列表视图,直到被告知停止。在更实际的场景中,线程可能会等待同步对象。当对象被信号触发时,线程可能会处理与控件关联的工作队列中的任何项。这是 UpdateListView
方法
private void UpdateListView()
{
ListView lv = null;
ListViewItem item = null;
string name = Thread.CurrentThread.Name;
int loopFor = 20;
int sleepFor = 25;
int count = 0;
switch (name)
{
case "Worker1":
lv = listView1;
break;
case "Worker2":
lv = listView2;
break;
case "Worker3":
lv = listView3;
break;
case "Worker4":
lv = listView4;
break;
}
// Keep running until we're told to stop.
while (run)
{
// Add n items to the list.
for (int i = 0; i < loopFor; ++i)
{
item = new ListViewItem(DateTime.Now.ToString("HH:mm:ss.ffff"));
item.SubItems.Add(string.Format("{0}: item {1}", name, ++count));
lv.Items.Insert(0,item);
Thread.Sleep(sleepFor);
}
// Now remove them.
for (int i = 0; i < loopFor; ++i)
{
lv.Items.RemoveAt(0);
Thread.Sleep(sleepFor);
}
}
}
运行示例应用程序
当您启动示例应用程序时,四个 ListView 控件将立即开始填充文本然后清空。在此期间,尝试调整窗体大小。更新状态栏文本或从主菜单显示模态“关于”对话框。拖动 ListViews 的列标题以重新排序它们。单击列标题几次以按升序或降序对列进行排序。所有这些都是线程安全的。
尽管 ListView 控件中有很多活动,但看看应用程序感觉多么响应。这是因为所有繁重的工作都在由与 ListView 控件关联的四个线程进行的后台处理。窗体上的所有其他控件都通过主 GUI 线程进行处理。在应用程序关闭之前,必须先按“结束循环”按钮,才能终止与四个 ListView 控件关联的四个线程。
性能
前提是小心确保只有与特定控件关联的线程才能更新其内容(无论是其项目集合还是数据源),这项技术就可以产生一个极其快速且响应迅速的 GUI。另一个好处是可以简化代码,因为不需要调用 InvokeRequired()
、BeginInvoke()
或 EndInvoke()
。此外,还将消除将调用封送到 GUI 线程的性能开销。如果 Windows Forms GUI 的速度是您的首要任务,您可能想尝试一下这项技术。
杂项说明
请记住,由于这项技术不常被使用,因此请确保您的代码添加了大量注释以保护无辜者。此代码是使用 Visual Studio 2005 开发的。
历史
- 2007 年 12 月 7 日 -- 原始版本发布
- 2007 年 12 月 18 日 -- 更新了示例代码和文本以阐明我的意图