日期:2010-01-31  浏览次数:20447 次

 异步编程是实现与程序其余部分并发运行的较大开销操作的一组技术。 常出现异步编程的一个领域是有图形化 UI 的程序环境:当开销较大的操作完成时,冻结 UI 通常是不可接受的。

此外,异步操作对于需要并发处理多个客户端请求的服务器应用程序来说非常重要。

在实践过程中出现的异步操作的典型例子包括向服务器发送请求并等待响应、从硬盘读取数据以及运行拼写检查等开销较大的计算。

以一个含 UI 的应用程序为例。

该应用程序可以使用 Windows Presentation Foundation (WPF) 或 Windows 窗体构建。

在此类应用程序中,大部分代码都在 UI 线程上执行,因为它为源自 UI 控件的事件执行事件处理程序。

当用户单击一个按钮时,UI 线程将选取该消息并执行 Click 事件处理程序。

现在,假设在 Click 事件处理程序中,应用程序将请求发送到服务器并等待响应:

// !!!
Bad code !!!
void Button_Click(object sender, RoutedEventArgs e) { 
 WebClient client = new WebClient(); 
 client.DownloadFile("http://www.microsoft.com", "index.html"); 
}

此代码中存在一个主要问题:下载网站需要几秒钟或更长时间。

接下来,调用 Button_Click 需要几秒钟才能返回。

这意味着 UI 线程会被阻止若干秒钟且 UI 会被冻结。

冻结界面会导致用户体验不佳,这种情况几乎都是不可接受的。

要使应用程序 UI 能随时响应,直到服务器做出响应,则需保证下载不是 UI 线程上的同步操作,这一点很重要。

让我们尝试一下解决冻结 UI 问题。

一个可能但并非最佳的解决方案是在不同线程上与服务器通信,以便 UI 线程保持未阻止状态。

下面是一个使用线程池线程与服务器通信的示例:

// Suboptimal code 
void Button_Click(object sender, RoutedEventArgs e) { 
 ThreadPool.QueueUserWorkItem(_ => { 
  WebClient client = new WebClient(); 
  client.DownloadFile( 
   "http://www.microsoft.com", "index.html"); 
 }); 
}

此代码示例解决了第一版存在的问题:现在 Button_Click 事件不会阻止 UI 线程,但基于线程的解决方案有三个严重问题。

让我们进一步了解一下这些问题。

问题 1:浪费线程池线程

我刚才介绍的解决方法使用来自线程池的线程将请求发送到服务器并等待服务器响应。

线程池线程将保持阻止状态,直到服务器响应。

在对 WebClient.DownloadFile 的调用完成之前,线程无法返回到线程池中。

由于 UI 不会冻结,因此阻止线程池线程比阻止 UI 线程要好得多,但它确实会浪费线程池的一个线程。

如果应用程序偶尔阻止线程池线程一段时间,性能损失可以忽略不计。

但是,如果应用程序经常阻止,其响应能力可能会因线程池承受的压力而降低。

线程池将尝试通过创建更多线程来应对这种情况,但会造成相当大的性能开销。

本文中介绍的所有其他异步编程模式可解决浪费线程池线程的问题。

问题 2:返回结果

使用线程进行异步编程的另一个难题是:从在帮助器线程上执行的操作返回值将变得略为凌乱。

在最初的示例中,DownloadFile 方法将下载的网页写入一个本地文件,因此它具有 void 返回值。

请看问题的另一个版本,您希望将收到的 HTML 指定到 TextBox(名为 HtmlTextBox)的 Text 属性中,而不是将下载的网页写入一个文件。

实现上述过程的一种想当然的错误方法如下:

// !!!
Broken code !!!
void Button_Click(object sender, RoutedEventArgs e) { 
 ThreadPool.QueueUserWorkItem(_ => { 
  WebClient client = new WebClient(); 
  string html = client.DownloadString( 
   "http://www.microsoft.com", "index.html"); 
  HtmlTextBox.Text = html; 
 }); 
}

问题在于 UI 控件 HtmlTextBox 被线程池线程修改。

这是一个错误,原因在于只有 UI 线程才有权修改 UI。

出于多种很充分的理由,WPF 和 Windows 窗体中都存在此限制。

要解决此问题,您可以在 UI 线程上捕获同步环境,然后在线程池线程上将消息发布到该环境:

void Button_Click(object sender, RoutedEventArgs e) { 
 SynchronizationContext ctx = SynchronizationContext.Current; 
 ThreadPool.QueueUserWorkItem(_ => { 
  WebClient client = new WebClient(); 
  string html = client.DownloadString( 
   "http://www.microsoft.com"); 
  ctx.Post(state => { 
   HtmlTextBox.Text = (string)state; 
  }, html); 
 }); 
}

认识到从帮助器线程返回值的问题不仅仅限于含 UI 的应用程序,这一点非常重要。

通常,从一个线程将值返回给另一个线程相当复杂,需要使用同步基元。

问题 3:组合异步操作

显式处理线程也使得组合异步操作变得困难。

例如,要并行下载多个网页,编写同步代码将变得更加困难,而且更容易出错。

此类实现将保留仍在执行的异步操作的计数器。

必须以线程安全的方式修改该计数器,比如说使用 Interlocked.Decrement。

一旦计数器到达零,处理下载的代码便会执行。

所有这一切都会导致相当大量的代码容易出错。

不用说,使用基于线程的模式甚至将更难正确实现更为复杂的复合模式。

基于事件的模式

使用 Microsoft .NET Framework 进行异步编程的一个常见模式是基于事件的模型。

事件模型公开一个方法,以便在操作完成时启动异步操作并引发一个事件。

事件模式是公开异步操作的一个惯例,但它不是通过接口之类的显式约定。

类实现器可以确定遵循模式的忠实程度。

图 1 显示了正确实现基于事件的异步编程模式所公开的方法示例。

图 1 基于事件的模式的方法

public class AsyncExample { 
 // Synchronous methods.
public int Method1(string param); 
 public void Method2(double param); 
 
 // Asynchronous methods.
public void Method1Async(string param); 
 public void Method1Async(string param, object userState); 
 public event Method1CompletedEventHandler Method1Completed; 
 
 public void Method2Async(double param); 
 public void Method2Async(double param, object userState); 
 public event Method2CompletedEventHandler Method2Completed; <