用户不喜欢反应慢的程序
程序反应越慢
就越没有用户会喜欢它
在执行耗时较长的操作时
使用多线程是明智之举
它可以提高程序 UI 的响应速度
使得一切运行显得更为快速
在 Windows 中进行多线程编程曾经是 C++ 开发人员的专属特权
但是现在
可以使用所有兼容 Microsoft
NET 的语言来编写
其中包括 Visual Basic
NET
不过
Windows 窗体对线程的使用强加了一些重要限制
本文将对这些限制进行阐释
并说明如何利用它们来提供快速
高质量的 UI 体验
即使是程序要执行的任务本身速度就较慢
为什么选择多线程?
多线程程序要比单线程程序更难于编写并且不加选择地使用线程也是导致难以找到细小错误的重要原因这就自然会引出两个问题为什么不坚持编写单线程代码?如果必须使用多线程如何才能避免缺陷呢?本文的大部分篇幅都是在回答第二个问题但首先我要来解释一下为什么确实需要多线程
多线程处理可以使您能够通过确保程序永不睡眠从而保持 UI 的快速响应大部分程序都有不响应用户的时候它们正忙于为您执行某些操作以便响应进一步的请求也许最广为人知的例子就是出现在打开文件对话框顶部的组合框如果在展开该组合框时CDROM驱动器里恰好有一张光盘则计算机通常会在显示列表之前先读取光盘这可能需要几秒钟的时间在此期间程序既不响应任何输入也不允许取消该操作尤其是在确实并不打算使用光驱的时候这种情况会让人无法忍受
执行这种操作期间 UI 冻结的原因在于UI 是个单线程程序单线程不可能在等待 CDROM驱动器读取操作的同时处理用户输入如图 所示打开文件对话框会调用某些阻塞 (blocking) API 来确定 CDROM 的标题阻塞 API 在未完成自己的工作之前不会返回因此这期间它会阻止线程做其他事情
图 单线程
在多线程下像这样耗时较长的任务就可以在其自己的线程中运行这些线程通常称为辅助线程因为只有辅助线程受到阻止所以阻塞操作不再导致用户界面冻结如图 所示应用程序的主线程可以继续处理用户的鼠标和键盘输入的同时受阻的另一个线程将等待 CDROM 读取或执行辅助线程可能做的任何操作
图 多线程
其基本原则是负责响应用户输入和保持用户界面为最新的线程(通常称为 UI 线程)不应该用于执行任何耗时较长的操作惯常做法是任何耗时超过 ms 的操作都要考虑从 UI 线程中移除这似乎有些夸张因为 ms 对于大多数人而言只不过是他们可以感觉到的最短的瞬间停顿实际上该停顿略短于电影屏幕中显示的连续帧之间的间隔
如果鼠标单击和相应的 UI 提示(例如重新绘制按钮)之间的延迟超过 ms那么操作与显示之间就会稍显不连贯并因此产生如同影片断帧那样令人心烦的感觉为了达到完全高质量的响应效果上限必须是 ms另一方面如果您确实不介意感觉稍显不连贯但也不想因为停顿过长而激怒用户则可按照通常用户所能容忍的限度将该间隔设为 ms
这意味着如果想让用户界面保持响应迅速则任何阻塞操作都应该在辅助线程中执行 — 不管是机械等待某事发生(例如等待 CDROM 启动或者硬盘定位数据)还是等待来自网络的响应
异步委托调用
在辅助线程中运行代码的最简单方式是使用异步委托调用(所有委托都提供该功能)委托通常是以同步方式进行调用即在调用委托时只有包装方法返回后该调用才会返回要以异步方式调用委托请调用 BeginInvoke 方法这样会对该方法排队以在系统线程池的线程中运行调用线程会立即返回而不用等待该方法完成这比较适合于 UI 程序因为可以用它来启动耗时较长的作业而不会使用户界面反应变慢
例如在以下代码中SystemWindowsFormsMethodInvoker 类型是一个系统定义的委托用于调用不带参数的方法
private void StartSomeWorkFromUIThread () { // The work we want to do is too slow for the UI // thread so lets farm it out to a worker thread MethodInvoker mi = new MethodInvoker( RunsOnWorkerThread); miBeginInvoke(null null); // This will not block}// The slow work is done here on a thread// from the system thread poolprivate void RunsOnWorkerThread() { DoSomethingSlow();}
如果想要传递参数可以选择合适的系统定义的委托类型或者自己来定义委托MethodInvoker 委托并没有什么神奇之处和其他委托一样调用 BeginInvoke 会使该方法在系统线程池的线程中运行而不会阻塞 UI 线程以便其可执行其他操作对于以上情况该方法不返回数据所以启动它后就不用再去管它如果您需要该方法返回的结果则 BeginInvoke 的返回值很重要并且您可能不传递空参数然而对于大多数 UI 应用程序而言这种启动后就不管的风格是最有效的稍后会对原因进行简要讨论您应该注意到BeginInvoke 将返回一个 IAsyncResult这可以和委托的 EndInvoke 方法一起使用以在该方法调用完毕后检索调用结果
还有其他一些可用于在另外的线程上运行方法的技术例如直接使用线程池 API 或者创建自己的线程然而对于大多数用户界面应用程序而言有异步委托调用就足够了采用这种技术不仅编码容易而且还可以避免创建并非必需的线程因为可以利用线程池中的共享线程来提高应用程序的整体性能
线程和控件
Windows 窗体体系结构对线程使用制定了严格的规则如果只是编写单线程应用程序则没必要知道这些规则这是因为单线程的代码不可能违反这些规则然而一旦采用多线程就需要理解 Windows 窗体中最重要的一条线程规则除了极少数的例外情况否则都不要在它的创建线程以外的线程中使用控件的任何成员
本规则的例外情况有文档说明但这样的情况非常少这适用于其类派生自 SystemWindowsFormsControl 的任何对象其中几乎包括 UI 中的所有元素所有的 UI 元素(包括表单本身)都是从 Control 类派生的对象此外这条规则的结果是一个被包含的控件(如包含在一个表单中的按钮)必须与包含它控件位处于同一个线程中也就是说一个窗口中的所有控件属于同一个 UI 线程实际中大部分 Windows 窗体应用程序最终都只有一个线程所有 UI 活动都发生在这个线程上这个线程通常称为 UI 线程这意味着您不能调用用户界面中任意控件上的任何方法除非在该方法的文档说明中指出可以调用该规则的例外情况(总有文档记录)非常少而且它们之间关系也不大请注意以下代码是非法的
// Created on UI threadprivate Label lblStatus;// Doesnt run on UI threadprivate void RunsOnWorkerThread() { DoSomethingSlow(); lblStatusText = Finished!; // BAD!!}
如果您在 NET Framework 版本中尝试运行这段代码也许会侥幸运行成功或者初看起来是如此这就是多线程错误中的主要问题即它们并不会立即显现出来甚至当出现了一些错误时在第一次演示程序之前一切看起来也都很正常但不要搞错 — 我刚才显示的这段代码明显违反了规则并且可以预见任何抱希望于试运行时良好应该就没有问题的人在即将到来的调试期是会付出沉重代价的
要注意在明确创建线程之前会发生这样的问题使用委托的异步调用实用程序(调用它的 BeginInvoke 方法)的任何代码都可能出现同样的问题委托提供了一个非常吸引人的解决方案来处理 UI 应用程序中缓慢阻塞的操作因为这些委托能使您轻松地让此种操作运行在 UI 线程外而无需自己创建新线程但是由于以异步委托调用方式运行的代码在一个来自线程池的线程中运行所以它不能访问任何 UI 元素上述限制也适用于线程池中的线程和手动创建的辅助线程
在正确的线程中调用控件
有关控件的限制看起来似乎对多线程编程非常不利如果在辅助线程中运行的某个缓慢操作不对 UI 产生任何影响用户如何知道它的进行情况呢?至少用户如何知道工作何时完成或者是否出现错误?幸运的是虽然此限制的存在会造成不便但并非不可逾越有多种方式可以从辅助线程获取消息并将该消息传递给 UI 线程理论上讲可以使用低级的同步原理和池化技术来生成自己的机制但幸运的是因为有一个以 Control 类的 Invoke 方法形式存在的解决方案所以不需要借助于如此低级的工作方式
Invoke 方法是 Control 类中少数几个有文档记录的线程规则例外之一它始终可以对来自任何线程的 Control 进行 Invoke 调用Invoke 方法本身只是简单地携带委托以及可选的参数列表并在 UI 线程中为您调用委托而不考虑 Invoke 调用是由哪个线程发出的实际上为控件获取任何方法以在正确的线程上运行非常简单但应该注意只有在 UI 线程当前未受到阻塞时这种机制才有效 — 调用只有在 UI 线程准备处理用户输入时才能通过从不阻塞 UI 线程还有另一个好理由Invoke 方法会进行测试以了解调用线程是否就是 UI 线程如果是它就直接调用委托否则它将安排线程切换并在 UI 线程上调用委托无论是哪种情况委托所包装的方法都会在 UI 线程中运行并且只有当该方法完成时Invoke 才会返回
Control 类也支持异步版本的 Invoke它会立即返回并安排该方法以便在将来某一时间在 UI 线程上运行这称为 BeginInvoke它与异步委托调用很相似与委托的明显区别在于该调用以异步方式在线程池的某个线程上运行然而在此处它以异步方式在 UI 线程上运行实际上Control 的 InvokeBeginInvoke 和 EndInvoke 方法以及 InvokeRequired 属性都是 ISynchronizeInvoke 接口的成员该接口可由任何需要控制其事件传递方式的类实现
由于 BeginInvoke 不容易造成死锁所以尽可能多用该方法而少用 Invoke 方法因为 Invoke 是同步的所以它会阻塞辅助线程直到 UI 线程可用但是如果 UI 线程正在等待辅助线程执行某操作情况会怎样呢?应用程序会死锁BeginInvoke 从不等待 UI 线程因而可以避免这种情况
现在我要回顾一下前面所展示的代码片段的合法版本首先必须将一个委托传递给 Control 的 BeginInvoke 方法以便可以在 UI 线程中运行对线程敏感的代码这意味着应该将该代码放在它自己的方法中如图 所示一旦辅助线程完成缓慢的工作后它就会调用 Label 中的 BeginInvoke以便在其 UI 线程上运行某段代码通过这样它可以更新用户界面
包装 ControlInvoke
虽然图 中的代码解决了这个问题但它相当繁琐如果辅助线程希望在结束时提供更多的反馈信息而不是简单地给出Finished!消息则 BeginInvoke 过于复杂的使用方法会令人生畏为了传达其他消息例如正在处理一切顺利等等需要设法向 UpdateUI 函数传递一个参数可能还需要添加一个进度栏以提高反馈能力这么多次调用 BeginInvoke 可能导致辅助线程受该代码支配这样不仅会造成不便而且考虑到辅助线程与 UI 的协调性这样设计也不好对这些进行分析之后我们认为包装函数可以解决这两个问题如图 所示
ShowProgress 方法对将调用引向正确线程的工作进行封装这意味着辅助线程代码不再担心需要过多关注 UI 细节而只要定期调用 ShowProgress 即可请注意我定义了自己的方法该方法违背了必须在 UI 线程上进行调用这一规则因为它进而只调用不受该规则约束的其他方法这种技术会引出一个较为常见的话题为什么不在控件上编写公共方法呢(这些方法记录为 UI 线程规则的例外)?
刚好 Control 类为这样的方法提供了一个有用的工具如果我提供一个设计为可从任何线程调用的公共方法则完全有可能某人会从 UI 线程调用这个方法在这种情况下没必要调用 BeginInvoke因为我已经处于正确的线程中调用 Invoke 完全是浪费时间和资源不如直接调用适当的方法为了避免这种情况Control 类将公开一个称为 InvokeRequired 的属性这是只限 UI 线程规则的另一个例外它可从任何线程读取如果调用线程是 UI 线程则返回假其他线程则返回真这意味着我可以按以下方式修改包装
public void ShowProgress(string msg int percentDone) { if (InvokeRequired) { // As before } else { // Were already on the UI thread just // call straight through UpdateUI(this new MyProgressEvents(msg PercentDone)); }}
ShowProgress 现在可以记录为可从任何线程调用的公共方法这并没有消除复杂性 — 执行 BeginInvoke 的代码依然存在它还占有一席之地不幸的是没有简单的方法可以完全摆脱它
锁定
任何并发系统都必须面对这样的事实即两个线程可能同时试图使用同一块数据有时这并不是问题 — 如果多个线程在同一时间试图读取某个对象中的某个字段则不会有问题然而如果有线程想要修改该数据就会出现问题如果线程在读取时刚好另一个线程正在写入则读取线程有可能会看到虚假值如果两个线程在同一时间在同一个位置执行写入操作则在同步写入操作发生之后所有从该位置读取数据的线程就有可能看到一堆垃圾数据虽然这种行为只在特定情况下才会发生读取操作甚至不会与写入操作发生沖突但是数据可以是两次写入结果的混加并保持错误结果直到下一次写入值为止为了避免这种问题必须采取措施来确保一次只有一个线程可以读取或写入某个对象的状态
防止这些问题出现所采用的方式是使用运行时的锁定功能C# 可以让您利用这些功能通过锁定关键字来保护代码(Visual Basic 也有类似构造称为 SyncLock)规则是任何想要在多个线程中调用其方法的对象在每次访问其字段时(不管是读取还是写入)都应该使用锁定构造例如请参见图
锁定构造的工作方式是公共语言运行库 (CLR) 中的每个对象都有一个与之相关的锁任何线程均可获得该锁但每次只能有一个线程拥有它如果某个线程试图获取另一个线程已经拥有的锁那么它必须等待直到拥有该锁的线程将锁释放为止C# 锁定构造会获取该对象锁(如果需要要先等待另一个线程利用它完成操作)并保留到大括号中的代码退出为止如果执行语句运行到块结尾该锁就会被释放并从块中部返回或者抛出在块中没有捕捉到的异常
请注意MoveBy 方法中的逻辑受同样的锁语句保护当所做的修改比简单的读取或写入更复杂时整个过程必须由单独的锁语句保护这也适用于对多个字段进行更新 — 在对象处于一致状态之前一定不能释放该锁如果该锁在更新状态的过程中释放则其他线程也许能够获得它并看到不一致状态如果您已经拥有一个锁并调用一个试图获取该锁的方法则不会导致问题出现因为单独线程允许多次获得同一个锁对于需要锁定以保护对字段的低级访问和对字段执行的高级操作的代码这非常重要MoveBy 使用 Position 属性它们同时获得该锁只有最外面的锁阻塞完成后该锁才会恰当地释放
对于需要锁定的代码必须严格进行锁定稍有疏漏便会功亏一篑如果一个方法在没有获取对象锁的情况下修改状态则其余的代码在使用它之前即使小心地锁定对象也是徒劳同样如果一个线程在没有事先获得锁的情况下试图读取状态则它可能读取到不正确的值运行时无法进行检查来确保多线程代码正常运行
死锁
锁是确保多线程代码正常运行的基本条件即使它们本身也会引入新的风险在另一个线程上运行代码的最简单方式是使用异步委托调用(请参见图 )
如果曾经调用过 Foo 的 CallBar 方法则这段代码会慢慢停止运行CallBar 方法将获得 Foo 对象上的锁并直到 BarWork 返回后才释放它然后BarWork 使用异步委托调用在某个线程池线程中调用 Foo 对象的 FooWork 方法接下来它会在调用委托的 EndInvoke 方法前执行一些其他操作EndInvoke 将等待辅助线程完成但辅助线程却被阻塞在 FooWork 中它也试图获取 Foo 对象的锁但锁已被 CallBar 方法持有所以FooWork 会等待 CallBar 释放锁但 CallBar 也在等待 BarWork 返回不幸的是BarWork 将等待 FooWork 完成所以 FooWork 必须先完成它才能开始结果没有线程能够进行下去
这就是一个死锁的例子其中有两个或更多线程都被阻塞以等待对方进行这里的情形和标准死锁情况还是有些不同后者通常包括两个锁这表明如果有某个因果性(过程调用链)超出线程界限就会发生死锁即使只包括一个锁!ControlInvoke 是一种跨线程调用过程的方法这是个不争的重要事实BeginInvoke 不会遇到这样的问题因为它并不会使因果性跨线程实际上它会在某个线程池线程中启动一个全新的因果性以允许原有的那个独立进行然而如果保留 BeginInvoke 返回的 IAsyncResult并用它调用 EndInvoke则又会出现问题因为 EndInvoke 实际上已将两个因果性合二为一避免这种情况的最简单方法是当持有一个对象锁时不要等待跨线程调用完成要确保这一点应该避免在锁语句中调用 Invoke 或 EndInvoke其结果是当持有一个对象锁时将无需等待其他线程完成某操作要坚持这个规则说起来容易做起来难
在检查代码的 BarWork 时它是否在锁语句的作用域内并不明显因为在该方法中并没有锁语句出现这个问题的唯一原因是 BarWork 调用自 FooCallBar 方法的锁语句这意味着只有确保正在调用的函数并不拥有锁时调用 ControlInvoke 或 EndInvoke 才是安全的对于非私有方法而言确保这一点并不容易所以最佳规则是根本不调用 ControlInvoke 和 EndInvoke这就是为什么启动后就不管的编程风格更可取的原因也是为什么 ControlBeginInvoke 解决方案通常比 ControlInvoke 解决方案好的原因
有时候除了破坏规则别无选择这种情况下就需要仔细严格地分析但只要可能在持有锁时就应该避免阻塞因为如果不这样死锁就难以消除
使其简单
如何既从多线程获益最大又不会遇到困扰并发代码的棘手错误呢?如果提高的 UI 响应速度仅仅是使程序时常崩溃那么很难说是改善了用户体验大部分在多线程代码中普遍存在的问题都是由要一次运行多个操作的固有复杂性导致的这是因为大多数人更善于思考连续过程而非并发过程通常最好的解决方案是使事情尽可能简单
UI 代码的性质是它从外部资源接收事件如用户输入它会在事件发生时对其进行处理但却将大部分时间花在了等待事件的发生如果可以构造辅助线程和 UI 线程之间的通信使其适合该模型则未必会遇到这么多问题因为不会再有新的东西引入我是这样使事情简单化的将辅助线程视为另一个异步事件源如同 Button 控件传递诸如 Click 和 MouseEnter 这样的事件可以将辅助线程视为传递事件(如 ProgressUpdate 和 WorkComplete)的某物只是简单地将这看作一种类比还是真正将辅助对象封装在一个类中并按这种方式公开适当的事件这完全取决于您后一种选择可能需要更多的代码但会使用户界面代码看起来更加统一不管哪种情况都需要 ControlBeginInvoke 在正确的线程上传递这些事件
对于辅助线程最简单的方式是将代码编写为正常顺序的代码块但如果想要使用刚才介绍的将辅助线程作为事件源模型那又该如何呢?这个模型非常适用但它对该代码与用户界面的交互提出了限制这个线程只能向 UI 发送消息并不能向它提出请求
例如让辅助线程中途发起对话以请求完成结果需要的信息将非常困难如果确实需要这样做也最好是在辅助线程中发起这样的对话而不要在主 UI 线程中发起该约束是有利的因为它将确保有一个非常简单且适用于两线程间通信的模型 — 在这里简单是成功的关键这种开发风格的优势在于在等待另一个线程时不会出现线程阻塞这是避免死锁的有效策略
图 显示了使用异步委托调用以在辅助线程中执行可能较慢的操作(读取某个目录的内容)然后将结果显示在 UI 上它还不至于使用高级事件语法但是该调用确实是以与处理事件(如单击)非常相似的方式来处理完整的辅助代码
取消
前面示例所带来的问题是要取消操作只能通过退出整个应用程序实现虽然在读取某个目录时 UI 仍然保持迅速响应但由于在当前操作完成之前程序将禁用相关按钮所以用户无法查看另一个目录如果试图读取的目录是在一台刚好没有响应的远程机器上这就很不幸因为这样的操作需要很长时间才会超时
要取消一个操作也比较困难尽管这取决于怎样才算取消一种可能的理解是停止等待这个操作完成并继续另一个操作这实际上是抛弃进行中的操作并忽略最终完成时可能产生的后果对于当前示例这是最好的选择因为当前正在处理的操作(读取目录内容)是通过调用一个阻塞 API 来执行的取消它没有关系但即使是如此松散的假取消也需要进行大量工作如果决定启动新的读取操作而不等待原来的操作完成则无法知道下一个接收到的通知是来自这两个未处理请求中的哪一个
支持取消在辅助线程中运行的请求的唯一方式是提供与每个请求相关的某种调用对象最简单的做法是将它作为一个 Cookie由辅助线程在每次通知时传递允许 UI 线程将事件与请求相关联通过简单的身份比较(参见图 )UI 代码就可以知道事件是来自当前请求还是来自早已废弃的请求
如果简单抛弃就行那固然很好不过您可能想要做得更好如果辅助线程执行的是进行一连串阻塞操作的复杂操作那么您可能希望辅助线程在最早的时机停止否则它可能会继续几分钟的无用操作在这种情况下调用对象需要做的就不止是作为一个被动 Cookie它至少还需要维护一个标记指明请求是否被取消UI 可以随时设置这个标记而辅助线程在执行时将定期测试这个标记以确定是否需要放弃当前工作
对于这个方案还需要做出几个决定如果 UI 取消了操作它是否要等待直到辅助线程注意到这次取消?如果不等待就需要考虑一个争用条件有可能 UI 线程会取消该操作但在设置控制标记之前辅助线程已经决定传递通知了因为 UI 线程决定不等待直到辅助线程处理取消所以 UI 线程有可能会继续从辅助线程接收通知如果辅助线程使用 BeginInvoke 异步传递通知则 UI 甚至有可能收到多个通知UI 线程也可以始终按与废弃做法相同的方式处理通知 — 检查调用对象的标识并忽略它不再关心的操作通知或者在调用对象中进行锁定并决不从辅助线程调用 BeginInvoke 以解决问题但由于让 UI 线程在处理一个事件之前简单地对其进行检查以确定是否有用也比较简单所以使用该方法碰到的问题可能会更少
请查看代码下载(本文顶部的链接)中的 AsyncUtils它是一个有用的基类可为基于辅助线程的操作提供取消功能图 显示了一个派生类它实现了支持取消的递归目录搜索这些类阐明了一些有趣的技术它们都使用 C# 事件语法来提供通知该基类将公开一些在操作成功完成取消和抛出异常时出现的事件派生类对此进行了扩充它们将公开通知客户端搜索匹配进度以及显示当前正在搜索哪个目录的事件这些事件始终在 UI 线程中传递实际上这些类并未限制为 Control 类 — 它们可以将事件传递给实现 ISynchronizeInvoke 接口的任何类图 是一个示例 Windows 窗体应用程序它为 Search 类提供一个用户界面它允许取消搜索并显示进度和结果
程序关闭
某些情况下可以采用启动后就不管的异步操作而不需要其他复杂要求来使操作可取消然而即使用户界面不要求取消有可能还是需要实现这项功能以使程序可以彻底关闭
当应用程序退出时如果由线程池创建的辅助线程还在运行则这些线程会被终止终止是简单粗暴的操作因为关闭甚至会绕开任何还起作用的 Finally 块如果异步操作执行的某些工作不应该以这种方式被打断则必须确保在关闭之前这样的操作已经完成此类操作可能包括对文件执行的写入操作但由于突然中断后文件可能被破坏
一种解决办法是创建自己的线程而不用来自辅助线程池的线程这样就自然会避开使用异步委托调用这样即使主线程关闭应用程序也会等到您的线程退出后才终止SystemThreadingThread 类有一个 IsBackground 属性可以控制这种行为它默认为 false这种情况下CLR 会等到所有非背景线程都退出后才正常终止应用程序然而这会带来另一个问题因为应用程序挂起时间可能会比您预期的长窗口都关闭了但进程仍在运行这也许不是个问题如果应用程序只是因为要进行一些清理工作才比正常情况挂起更长时间那没问题另一方面如果应用程序在用户界面关闭后还挂起几分钟甚至几小时那就不可接受了例如如果它仍然保持某些文件打开则可能妨碍用户稍后重启该应用程序
最佳方法是如果可能通常应该编写自己的异步操作以便可以将其迅速取消并在关闭应用程序之前等待所有未完成的操作完成这意味着您可以继续使用异步委托同时又能确保关闭操作彻底且及时
错误处理
在辅助线程中出现的错误一般可以通过触发 UI 线程中的事件来处理这样错误处理方式就和完成及进程更新方式完全一样因为很难在辅助线程上进行错误恢复所以最简单的策略就是让所有错误都为致命错误错误恢复的最佳策略是使操作完全失败并在 UI 线程上执行重试逻辑如果需要用户干涉来修复造成错误的问题简单的做法是给出恰当的提示
AsyncUtils 类处理错误以及取消如果操作抛出异常该基类就会捕捉到并通过 Failed 事件将异常传递给 UI
小结
谨慎地使用多线程代码可以使 UI 在执行耗时较长的任务时不会停止响应从而显着提高应用程序的反应速度异步委托调用是将执行速度缓慢的代码从 UI 线程迁移出来从而避免此类间歇性无响应的最简单方式
Windows Forms Control 体系结构基本上是单线程但它提供了实用程序以将来自辅助线程的调用封送返回至 UI 线程处理来自辅助线程的通知(不管是成功失败还是正在进行的指示)的最简单策略是以对待来自常规控件的事件(如鼠标单击或键盘输入)的方式对待它们这样可以避免在 UI 代码中引入新的问题同时通信的单向性也不容易导致出现死锁
有时需要让 UI 向一个正在处理的操作发送消息其中最常见的是取消一个操作通过建立一个表示正在进行的调用的对象并维护由辅助线程定期检查的取消标记可实现这一目的如果用户界面线程需要等待取消被认可(因为用户需要知道工作已确实终止或者要求彻底退出程序)实现起来会有些复杂但所提供的示例代码中包含了一个将所有复杂性封装在内的基类派生类只需要执行一些必要的工作周期性测试取消以及要是因为取消请求而停止工作就将结果通知基类