您的位置:首页 > 其它

通过多线程为基于.NET的应用程序实现响应迅速(4)

2006-08-25 23:03 211 查看
前面示例所带来的问题是,要取消操作只能通过退出整个应用程序实现。虽然在读取某个目录时 UI 仍然保持迅速响应,但由于在当前操作完成之前程序将禁用相关按钮,所以用户无法查看另一个目录。如果试图读取的目录是在一台刚好没有响应的远程机器上,这就很不幸,因为这样的操作需要很长时间才会超时。

要取消一个操作也比较困难,尽管这取决于怎样才算取消。一种可能的理解是“停止等待这个操作完成,并继续另一个操作。”这实际上是抛弃进行中的操作,并忽略最终完成时可能产生的后果。对于当前示例,这是最好的选择,因为当前正在处理的操作(读取目录内容)是通过调用一个阻塞 API 来执行的,取消它没有关系。但即使是如此松散的“假取消”也需要进行大量工作。如果决定启动新的读取操作而不等待原来的操作完成,则无法知道下一个接收到的通知是来自这两个未处理请求中的哪一个。

支持取消在辅助线程中运行的请求的唯一方式是,提供与每个请求相关的某种调用对象。最简单的做法是将它作为一个 Cookie,由辅助线程在每次通知时传递,允许 UI 线程将事件与请求相关联。通过简单的身份比较(参见图 8),UI 代码就可以知道事件是来自当前请求,还是来自早已废弃的请求。

如果简单抛弃就行,那固然很好,不过您可能想要做得更好。如果辅助线程执行的是进行一连串阻塞操作的复杂操作,那么您可能希望辅助线程在最早的时机停止。否则,它可能会继续几分钟的无用操作。在这种情况下,调用对象需要做的就不止是作为一个被动 Cookie。它至少还需要维护一个标记,指明请求是否被取消。UI 可以随时设置这个标记,而辅助线程在执行时将定期测试这个标记,以确定是否需要放弃当前工作。

对于这个方案,还需要做出几个决定:如果 UI 取消了操作,它是否要等待直到辅助线程注意到这次取消?如果不等待,就需要考虑一个争用条件:有可能 UI 线程会取消该操作,但在设置控制标记之前辅助线程已经决定传递通知了。因为 UI 线程决定不等待,直到辅助线程处理取消,所以 UI 线程有可能会继续从辅助线程接收通知。如果辅助线程使用 BeginInvoke 异步传递通知,则 UI 甚至有可能收到多个通知。UI 线程也可以始终按与“废弃”做法相同的方式处理通知 — 检查调用对象的标识并忽略它不再关心的操作通知。或者,在调用对象中进行锁定并决不从辅助线程调用 BeginInvoke 以解决问题。但由于让 UI 线程在处理一个事件之前简单地对其进行检查以确定是否有用也比较简单,所以使用该方法碰到的问题可能会更少。

请查看“代码下载”(本文顶部的链接)中的 AsyncUtils,它是一个有用的基类,可为基于辅助线程的操作提供取消功能。图 9 显示了一个派生类,它实现了支持取消的递归目录搜索。这些类阐明了一些有趣的技术。它们都使用 C# 事件语法来提供通知。该基类将公开一些在操作成功完成、取消和抛出异常时出现的事件。派生类对此进行了扩充,它们将公开通知客户端搜索匹配、进度以及显示当前正在搜索哪个目录的事件。这些事件始终在 UI 线程中传递。实际上,这些类并未限制为 Control 类 — 它们可以将事件传递给实现 ISynchronizeInvoke 接口的任何类。图 10 是一个示例 Windows 窗体应用程序,它为 Search 类提供一个用户界面。它允许取消搜索并显示进度和结果。

程序关闭

某些情况下,可以采用“启动后就不管”的异步操作,而不需要其他复杂要求来使操作可取消。然而,即使用户界面不要求取消,有可能还是需要实现这项功能以使程序可以彻底关闭。

当应用程序退出时,如果由线程池创建的辅助线程还在运行,则这些线程会被终止。终止是简单粗暴的操作,因为关闭甚至会绕开任何还起作用的 Finally 块。如果异步操作执行的某些工作不应该以这种方式被打断,则必须确保在关闭之前这样的操作已经完成。此类操作可能包括对文件执行的写入操作,但由于突然中断后,文件可能被破坏。

一种解决办法是创建自己的线程,而不用来自辅助线程池的线程,这样就自然会避开使用异步委托调用。这样,即使主线程关闭,应用程序也会等到您的线程退出后才终止。System.Threading.Thread 类有一个 IsBackground 属性可以控制这种行为。它默认为 false,这种情况下,CLR 会等到所有非背景线程都退出后才正常终止应用程序。然而,这会带来另一个问题,因为应用程序挂起时间可能会比您预期的长。窗口都关闭了,但进程仍在运行。这也许不是个问题。如果应用程序只是因为要进行一些清理工作才比正常情况挂起更长时间,那没问题。另一方面,如果应用程序在用户界面关闭后还挂起几分钟甚至几小时,那就不可接受了。例如,如果它仍然保持某些文件打开,则可能妨碍用户稍后重启该应用程序。

最佳方法是,如果可能,通常应该编写自己的异步操作以便可以将其迅速取消,并在关闭应用程序之前等待所有未完成的操作完成。这意味着您可以继续使用异步委托,同时又能确保关闭操作彻底且及时。

错误处理

在辅助线程中出现的错误一般可以通过触发 UI 线程中的事件来处理,这样错误处理方式就和完成及进程更新方式完全一样。因为很难在辅助线程上进行错误恢复,所以最简单的策略就是让所有错误都为致命错误。错误恢复的最佳策略是使操作完全失败,并在 UI 线程上执行重试逻辑。如果需要用户干涉来修复造成错误的问题,简单的做法是给出恰当的提示。

AsyncUtils 类处理错误以及取消。如果操作抛出异常,该基类就会捕捉到,并通过 Failed 事件将异常传递给 UI。

小结

谨慎地使用多线程代码可以使 UI 在执行耗时较长的任务时不会停止响应,从而显著提高应用程序的反应速度。异步委托调用是将执行速度缓慢的代码从 UI 线程迁移出来,从而避免此类间歇性无响应的最简单方式。

Windows Forms Control 体系结构基本上是单线程,但它提供了实用程序以将来自辅助线程的调用封送返回至 UI 线程。处理来自辅助线程的通知(不管是成功、失败还是正在进行的指示)的最简单策略是,以对待来自常规控件的事件(如鼠标单击或键盘输入)的方式对待它们。这样可以避免在 UI 代码中引入新的问题,同时通信的单向性也不容易导致出现死锁。

有时需要让 UI 向一个正在处理的操作发送消息。其中最常见的是取消一个操作。通过建立一个表示正在进行的调用的对象并维护由辅助线程定期检查的取消标记可实现这一目的。如果用户界面线程需要等待取消被认可(因为用户需要知道工作已确实终止,或者要求彻底退出程序),实现起来会有些复杂,但所提供的示例代码中包含了一个将所有复杂性封装在内的基类。派生类只需要执行一些必要的工作、周期性测试取消,以及要是因为取消请求而停止工作,就将结果通知基类。

(责任编辑:海纳百川 qlmzl11268@hotmail.com TEL:(010)68476606-8007)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐