/ async

async

Async (异步编程)。

如果一个程序调用某个方法,等待其执行所有处理后才继续执行,我们就称这样的方法是 同步 的。这是默认的形式。

相反,异步 的方法在处理完成之前就返回到调用的方法。

C# 中的异步编程

C# 的 async/await特性可以创建并使用异步方法。该特性由三个部分组成:

  • 调用方法:该方法调用异步方法,然后在异步方法(可能在相同的线程,也可能在不同的线程)执行其任务的时候继续执行。
  • 异步(async)方法:该方法异步执行其工作,然后立即返回到调用方法。
  • await 表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个 await 表达式,不过如果一个都不包含的话,编译器会发出警告。
class Program
{
  static void Main()
  {
    ...
    Task<int> value = DoAsyncStuff.CalculateSumAsync(5,6);
    ...
  }
}

static class DoAsyncStuff
{
  public static async Task<int> CalculateSumAsync(int i1, int i2)
  {
    int sum = await TaskEx.Run( () => GetSum( i1, i2) );
    return sum;
  }
   
  private static int GetSum(int i1, int i2) { return i1 + i2; }
}

在 GUI 程序中执行异步操作

异步方法在 GUI 程序中尤为有用。原因是 GUI 程序在设计上就要求所有的显示变化都必须在主 GUI 线程中完成,如点击按钮、展示标签、移动窗体等。Windows 是通过消息来实现这一点的,消息被放入由 消息泵 管理的 消息队列 中。

消息泵从队列中取出一条消息,并调用它的处理程序(handler)代码。当处理程序完成时,消息泵获取下一条消息并循环整个过程。

由于这种架构,处理程序代码就必须耗时短,这样才不至于挂起并阻碍其他 GUI 行为的处理。如果某个消息的处理程序代码耗时过长,消息队列中的消息就会产生积压。程序将失去响应,因为在那个长时间运行的处理程序完成之前,无法处理任何消息。

演示程序:用户点击按钮,按钮的处理程序代码执行以下操作:

  • 禁用按钮,这样在处理程序执行期间用户就不能再次点击了;
  • 将标签文本改为 Doing Stuff,这样用户就会知道程序正在运行了;
  • 让程序休眠4秒钟——模拟某个工作;
  • 将标签文本改为原始文本,并启用按钮。
private void btnDoStuff_Click( object sender, RoutedEventArgs e)
{
  btnDoStuff.IsEnabled = false;
  lblStatus.Content    = "Doing Stuff";

  Thread.Sleep( 4000 );

  lblStatus.Content    = "Not Doing Anything";
  btnDoStuff.IsEnabled = true;
}

运行程序后,会发现程序并没有按照我们预期的执行:按钮没有禁用,状态标签也没有改变,在4秒之前窗体也无法移动。如果在点击按钮后移动窗体,会发现它已经冻结,不会移动——直到4秒之后,窗体才突然出现在新位置。

这是因为在处理程序本身退出(即休眠4秒并退出)之前,这些消息(禁用按钮、改变文本、移动窗体)都无法执行。然后所有的行为都发生了,但速度太快肉眼根本看不见。

但是,如果处理程序能将前两条消息压如队列,然后将自己从处理器上摘下,在4秒之后再将自己压入队列,那么这些以及所有其他消息都可以在等待的时间内被处理,整个过程都会如我们之前预料的那样,并且还能保持响应。

private async void btnDoStuff_Click(object sender, RoutedEventArgs e)
{
    btnDoStuff.IsEnabled = false;
    lblStatus.Content = "Doing Stuff";

    await Task.Delay(4000);

    lblStatus.Content = "Not Doing Anything";
    btnDoStuff.IsEnabled = true;
}