C# 异步编程 (12)

异步编程重要性

C# 5.0 提供了更强大的异步编程。添加两个新的关键字 async 和 await 。

使用异步编程,方法调用是在后台运行(通常在线程或任务的帮助下),并且不会阻塞调用线程。

3种不同模式的异步编程:异步模式、基于事件的异步模式 和 新增加的基于任务的异步模式(TAP)。TAP 是利用 async 和 await 关键字来实现的。

如果后台任务执行时间较长,可以通过取消任务,来防止卡顿。应用程序没有立刻相应用户的请求,会让用户反感。用鼠标操作,我们习惯了出现延迟,但是触摸UI,应用程序要求立刻响应用户的请求,否则,用户就会不断重复同一个动作。

现在很多 .NET FrameWork 的 API 都提供了 同步版本 和 异步版本 。如果一个 API 调用时间超过 40ms, 就只能使用其异步版本。 .NET 4.5中 ,同步编程 和 异步编程 很简单。

异步模式

在 Windows Forms 和 WPF 中,用异步模式更新界面非常复杂(利用委托类型实现的异步模式),所以之后出现基于事件的异步模式。事件处理程序是被拥有同步上下文的线程调用,所以更新界面很容易用这种模式处理。这种模式也称为 异步组件模式。

在.NET 4.5 中,推出了基于任务的异步模式(TAP)。通过 Task 类型、async 和 await 关键字来实现。

同步调用

  // 同步调用
  // 用URL属性发出WebClient类的HTTP请求
  // DownloadString方法会阻塞,直到收到结果
  // 然后再通过 Parse 解析

  // 当运行时,用户界面会被阻塞,直到 OnSearchSync 方法对Bing 和 Filckr 的网络调用。调用所需的时间取决于网络速度,以及 Bing 与 Flickr 的工作量。
  // 对于用户而言,等待是非常不愉快的。
private void OnSearchSync(object sender, RoutedEventArgs e)
{
  foreach (var req in GetSearchRequests())
  {
    WebClient client = new WebClient();
    client.Credentials = req.Credentials;
    string resp = client.DownloadString(req.Url);
    IEnumerable<SearchItemResult> images = req.Parse(resp);
    foreach (var image in images)
    {
      searchInfo.List.Add(image);
    }
  }
}

异步调用之异步模式

异步模式定义了 BeginXXX 方法 和 EndXXX 方法。例如同步方法 DownloadString,异步就是 BeginDownloadString 和 EndDownloadString 方法。BeginXXX 方法接受其同步方法所有输入参数,EndXXX方法是用同步方法所有输出的参数,并按照同步方法的返回类型返回结果。使用异步模式时,BeginXXX方法还定义了一个AsyncCallback参数,用于接受在异步方法执行完成后调用的委托。BeginXXX方法返回IAsyncResult,用于验证调用是否已经完成,并且一直等到方法的执行结束。

WebClient 没有异步模式,可以用 HttpWebRequest 替代,通过 BeginGetResponse 和 EndGetResponse 方法。

下面的示例,利用的委托实现的异步模式。

委托类型定义了 Invoke 方法用于调用同步方法,还定义了BeginInvoke 和 EndInvolve方法,用于使用异步模式。 声明 Func<string,string>类型的委托 downloadstring 引用一个 string 参数 和一个 string 返回值 的方法。downloadstring 变量引用的方法是用 lambda 表达式实现的,并且用调用 WebClient 类型的同步方法DownloadString。这个委托通过调用BeginInvole方法来异步调用。这个方法是使用线程池中的一个线程来继续异步调用。

BeginInvoke 方法第一个参数是Func委托的第一个字符串泛型参数,用于传递Url。第二个参数类型是 AsyncCallback。AsyncCallback 是一个委托,需要 IAsyncResult作为参数。当异步方法执行完毕后,将调用这个委托引用的方法。之后会调用 downloadString.EndInvoke 来检索结果,其方式与以前解析 XML 内容和获得集合项的方式相同。但是,这里不能直接把结果返回给 UI,因为UI绑定到一个单独的线程。而回调在一个后台的线程。所以必须使用窗口的 Dispatcher 属性切换回 UI 线程。 Dispatcher 的 Invoke 方法需要一个委托作为参数,这个就是定义 Action<SearchItemResult> 的原因。

  // 异步调用之一 (异步模式)
private void OnSeachAsyncPattern(object sender, RoutedEventArgs e)
{
  Func<string, ICredentials, string> downloadString = (address, cred) =>
    {
      var client = new WebClient();
      client.Credentials = cred;
      return client.DownloadString(address);
    };

  Action<SearchItemResult> addItem = item => searchInfo.List.Add(item);

  foreach (var req in GetSearchRequests())
  {
    downloadString.BeginInvoke(req.Url, req.Credentials, ar =>
      {
        string resp = downloadString.EndInvoke(ar);
        var images = req.Parse(resp);
        foreach (var image in images)
        {
          this.Dispatcher.Invoke(addItem, image);
        }
      }, null);
  }
}

BeginInvoke 最后一个参数是格式字符串,传递给 ar.AsyncState 属性。

http://cdlgdxgcjsxy2.blog.163.com/blog/static/16936188720105140195591/

https://msdn.microsoft.com/zh-cn/library/system.iasyncresult.asyncstate(v=vs.110).aspx

https://msdn.microsoft.com/en-us/library/2e08f6yc(v=vs.110).aspx

异步模式的优势是使用委托功能实现异步编程。程序不会阻塞UI。但是有点复杂。

基于事件的异步

基于事件的异步模式定义了一个带有“Async”后缀的方法,如 同步方法 DownloadString,WebClient 对应的 DownloadStringAsync。 当异步方法 DownloadStringAsync 完成,会调用 DowloadStringCompleted 事件。

 private void OnAsyncEventPattern(object sender, RoutedEventArgs e)
 {
   foreach (var req in GetSearchRequests())
   {
     var client = new WebClient();
     client.Credentials = req.Credentials;
       // 添加事件
     client.DownloadStringCompleted += (sender1, e1) =>
       {
           // sender1 事件发送者
           // e1      事件参数
         string resp = e1.Result;
         var images = req.Parse(resp);
         foreach (var image in images)
         {
           searchInfo.List.Add(image);
         }
       };
       // 调用异步事件方法
     client.DownloadStringAsync(new Uri(req.Url));
   }
 }

基于事件的异步模式优势容易使用。

添加自定义事件

https://msdn.microsoft.com/zh-cn/library/ak9w5846.aspx

基于任务的异步模式

在.NET 4.5 中,更新了WebClient类,提供基于任务的异步模式(TAP)。它提供一个方法 DownloadStringTaskAsync 。

private async void OnTaskBasedAsyncPattern1(object sender, RoutedEventArgs e)
{
  foreach (var req in GetSearchRequests())
  {
    var client = new WebClient();
    client.Credentials = req.Credentials;
    // DownloadStringTaskAsync 返回 Task<string>
    // 不需要声明 Task<string> 类型 来 赋值返回结果,只需要 await 关键字 和 声明一个 string 类型的变量。
    // await 关键字会解除(UI线程)的阻塞。 当 DownloadStringTaskAsync 完成后,继续往下执行。
    string resp = await client.DownloadStringTaskAsync(req.Url);

    var images = req.Parse(resp);
    foreach (var image in images)
    {
      searchInfo.List.Add(image);
    }
  }
}

async 关键字创建一个状态机,类似 yield return 语句。

下面用HttpClient类实现的基于任务的异步模式。

private async void OnTaskBasedAsyncPattern(object sender, RoutedEventArgs e)
{
  cts = new CancellationTokenSource();
  try
  {
    foreach (var req in GetSearchRequests())
    {
      var clientHandler = new HttpClientHandler
      {
         Credentials = req.Credentials
      };
      var client = new HttpClient(clientHandler);

        // 使用 GetAsync 发出异步请求
      var response = await client.GetAsync(req.Url, cts.Token);
        // 异步 返回字符串格式的内容
      string resp = await response.Content.ReadAsStringAsync();

      // 解析XML 可能需要一段时间,用 Task.Run 同步功能创建后台任务
      await Task.Run(() =>
      {
        var images = req.Parse(resp);
        foreach (var image in images)
        {
          cts.Token.ThrowIfCancellationRequested();
          searchInfo.List.Add(image);
        }
      }, cts.Token);
    }
  }
  catch (OperationCanceledException ex)
  {
    MessageBox.Show(ex.Message);
  }
}

因为传递给 Task.Run 方法的代码块在后头线程上运行,所以这里的问题和以前引用UI代码相同。在.net 4.5中,wpf 提供 可以在后台线程上填充绑定 UI 的 集合。如

 private object lockList = new object();

 public MainWindow()
 {
// 在后台线程填充绑定UI集合 BindingOperations.EnableCollectionSynchronization(searchInfo.List, lockList); }

异步编程的基础

async 和 await 关键字只是编译器功能。编译器会用Task类创建代码。如果不使用这两个关键字也可以用C# 4.0 Task 类的方法实现同样的功能。

创建任务

// 创建一个同步方法 3秒后,返回一个字符串
public static string Greeting(string name)
{
    // 挂起线程3秒钟
    Thread.Sleep(3000);
    return string.Format("Hello, {0}", name);
}

// 定义基于任务的异步模式指定
// 异步方法 GreetingAsync 和 同步方法 Greeting 具有相同的输入参数,区别他返回的是 Task<string>
// Task<string> 定义了一个返回字符串的任务,这里用的是 泛型版本 Task.Run<string> 方法返回的字符串的任务
public static Task<string> GreetingAsync(string name)
{
    return Task.Run<string>(() =>
    {
        return Greeting(name);
    });
}

调用异步方法

 // 使用await关键调用返回任务的异步方法 GreetingAsync , 用 async 修饰符声明方法。
 // 只有 GreetingAsync 方法完成之后,才往后执行。并该线程没有阻塞。
 private async static void CallerWithAsync()
 {
     string result = await GreetingAsync("Stephanie");
     Console.WriteLine(result);
 }

 // 也可以这样
 private async static void CallerWithAsync2()
 {
     Console.WriteLine(await GreetingAsync("Stephanie"));
 }

async 修饰符只能用于返回 Task 或 void 方法。不用用于程序入口点,即 Main方法。

await  只能用于返回Task的方法。

延续任务

// ContinueWith 定义 任务完成后调用的代码 将已完成的任务作为参数传入,任务返回的结果 用 Result 属性访问。
private static void CallerWithContinuationTask()
{
    Task<string> t1 = GreetingAsync("Stephanie");
    t1.ContinueWith(t =>
    {
        string result = t.Result;
        Console.WriteLine(result);
    });
}

编译器把await关键字后的所有代码放进ContinueWith方法的代码块来转换await关键字。

同步上下文

      如果验证一下方法中使用的线程,会发现CallerWithAsync方法和CallerWithContinuationTask方 法,在方法的不同生命阶段使用了不同的线程。一个线程用于调用GreetingAsync方法,另外一个 线程执行await关键字后面的代码,或者继续执行ContinueWith方法内的代码块。
      使用一个控制台应用程序,通常不会有什么问题。但是,必须保证在所有应该完成的后台任务 完成之前,至少有一个前台线程仍然在运行。示例应用程序调用Console.ReadLine来保证主线程一 直在运行,直到按下返回键。

      为了执行某些动作,有些应用程序会绑定到指定的线程上(例如,在WPF应用程序中,只有UI 线程才能访问UI元素),这将会是一个问题。

如果使用async和await关键字,当await完成之后,不需要进行任何特别处理,就能访问UI 线程。默认情况下,生成的代码就会把线程转换到拥有同步上下文的线程中。

WPF应用程序设置了 DispatcherSynchronizationContext 属性,WmdowsForm 应用程序设置了 WindowsFormsSynchronization- Context属性。如果调用异步方法的线程分配给了同步上下文,await完成之后将继续执行。默认情 况下,使用了同步上下文。

如果不使用相同的同步上下文,必须调用 Task 类的 ConfigureAwait (continueOnCapturedContext: false)。例如,一个WPF应用程序,其await后面的代码没有用到任何的UI元素。在这种情况下,避免切换到同步上下文会执行得更快。

使用多个异步方法

1、按顺序调用异步方法

private async static void MultipleAsyncMethods()
{
  string s1 = await GreetingAsync("Stephanie");
  string s2 = await GreetingAsync("Matthias");
  Console.WriteLine("Finished both methods.\n Result 1: {0}\n Result 2: {1}", s1, s2);
}

2、使用组合器

示例调用 Task.WhenAll 组合器, 它可以等待,直到两个任务都完成。

private async static void MultipleAsyncMethodsWithCombinators1()
{
  Task<string> t1 = GreetingAsync("Stephanie");
  Task<string> t2 = GreetingAsync("Matthias");
  await Task.WhenAll(t1, t2);
  Console.WriteLine("Finished both methods.\n Result 1: {0}\n Result 2: {1}", t1.Result, t2.Result);
}

private async static void MultipleAsyncMethodsWithCombinators2()
{
  Task<string> t1 = GreetingAsync("Stephanie");
  Task<string> t2 = GreetingAsync("Matthias");
  string[] result = await Task.WhenAll(t1, t2);
  Console.WriteLine("Finished both methods.\n Result 1: {0}\n Result 2: {1}", result[0], result[1]);
}

Task类定义了WhenAll 和 WhenAny组合器。从 WhenAll 方法返回的Task,是在所有传入方法的任务都完成了才会返回Task。从WhenAny 返回的Task ,是在其中一个传入方法的任务完成了就会返回Task。

Task类型的WhenAll方法定义了几个重载版本。如果所有的任务返回相同的类型,那么该类型的数组可以用于 await 返回的结果。 GreetingAsync 方法返回一个 Task<string> 等待返回的结果是一个字符串(string)形式。 因此,Task.WhenAll 可以用于返回一个字符串数组。

转换异步模式

首先,从前面定义的同步方法 Greeting 中,借助于委托,创建一个异步方法。Greeting 方法接收一个字符串作为参数,并返回一个字符串。因此,Func<string,string>委托的变量可用于引用Greeting方法。按照异步模式,BeginGreeting 方法接收一个 string 参数,一个 AsyncCallback 参数 和 一个 object 参数,返回 IAsyncResult。 EndGreeting 方法返回来自 Greeting 方法的结果,一个字符串并接收一个 IAsyncResult 参数。在实现代码中,该委托仅用于异步执行任务。

   // BeginGreeting 和 EndGreeting 方法,它们都应转换为使用 async 和 await 关键字来获取结果。
   // TaskFactory 类定义了 FromAsync 方法。把使用异步模式的方法转换为基于任务的异步模式的方法。
 private static async void ConvertingAsyncPattern()
 {
     // FromAsync 方法,前面两个是 委托类型 传入 BeginGreeting 和 EndGreeting 方法的地址。后面两个是 输入的参数 和 对象状态参数。
     // 返回 Task 类型,所以可以用 await 。
   string r = await Task<string>.Factory.FromAsync<string>(BeginGreeting, EndGreeting, "Angela", null);
   Console.WriteLine(r);
 }

private static Func<string, string> greetingInvoker = Greeting;

static IAsyncResult BeginGreeting(string name, AsyncCallback callback, object state)
{
  return greetingInvoker.BeginInvoke(name, callback, state);
}

static string EndGreeting(IAsyncResult ar)
{
  return greetingInvoker.EndInvoke(ar);
}

错误处理

使用异步方法时,需要对错误进行特殊处理。

static async Task ThrowAfter(int ms, string message)
{
  await Task.Delay(ms);
  throw new Exception(message);
}

private static void DontHandle()
{
  try
  {
    ThrowAfter(200, "first");
  }
  catch (Exception ex)
  {
    Console.WriteLine(ex.Message);
  }
}

如果调用异步方法,并没有等待,将异步放在 try/catch中,并不会捕获到异常。因为DontHandle方法在ThrowAfter抛出异常之前,已经执行完毕。需要等待ThrowAfter方法(用await关键字)。

异步方法的异常处理

private static async void HandleOneError()
{
  try
  {
    await ThrowAfter(2000, "first");
  }
  catch (Exception ex)
  {
    Console.WriteLine("handled {0}", ex.Message);
  }
}

异步调用ThrowAfter方法之后,HandleOneError方法就好释放线程,但它会在任务完成时保持任务的引用。2秒后,抛出异常,会调用匹配的 catch 块内的代码。

多个异步方法的异常处理

private static async void StartTwoTasks()
{
  try
  {
    await ThrowAfter(2000, "first");
    await ThrowAfter(1000, "second"); 
  }
  catch (Exception ex)
  {
    Console.WriteLine("handled {0}", ex.Message);
  }
}

第一个 ThrowAfter 方法被调用,2秒抛出 first 异常。结束后,并没有继续调用第二个 ThrowAfter 方法。因为  catch 块内 已经对第一个异常进行处理了。

现在我们已并行的方式调用这两个方法,使用 Task.WhenAll,不管任务是否抛出异常,都会等到两个任务完成。

private async static void StartTwoTasksParallel()
{
  try
  {
    Task t1 = ThrowAfter(2000, "first");
    Task t2 = ThrowAfter(1000, "second");
    await Task.WhenAll(t1, t2);
  }
  catch (Exception ex)
  {
    Console.WriteLine("handled {0}", ex.Message);
  }
}

等待2秒后,却发现只输出了 first 异常,还是没有输出第二个异常。

获取所有任务的异常信息

解决方法一

private async static void StartTwoTasksParallel()
{
    Task t1 = null;
    Task t2 = null;
  try
  {
    t1 = ThrowAfter(2000, "first");
    t2 = ThrowAfter(1000, "second");
    await Task.WhenAll(t1, t2);
  }
  catch (Exception ex)
  {
      // 检查是否有出错状态
      if (t1.IsFaulted)
      {
          Console.WriteLine("t1 handled {0}", t1.Exception.InnerException.Message);
      }

      if (t2.IsFaulted)
      {
          Console.WriteLine("t2 handled {0}", t2.Exception.InnerException.Message);
      }
  }
}

解决方法二

将 Task.WhenAll 返回结果 赋值给 Task 类型变量。

private static async void ShowAggregatedException()
{
  Task taskResult = null;
  try
  {
    Task t1 = ThrowAfter(2000, "first");
    Task t2 = ThrowAfter(1000, "second");
    await (taskResult = Task.WhenAll(t1, t2));
  }
  catch (Exception ex)
  {
    Console.WriteLine("handled {0}", ex.Message);
    foreach (var ex1 in taskResult.Exception.InnerExceptions)
    {
      Console.WriteLine("inner exception {0} from task {1}", ex1.Message, ex1.Source);
    }
  }
}

取消任务

取消基于 CancellationTokenSource 类,该类可用于发送取消请求。请求发送给引用 CancellationToken 类的任务,其中 CancellationToken 类与 CancellationTokenSource 类相关联。

private CancellationTokenSource cts = new CancellationTokenSource();

// 取消任务
cts.Cancel();
// 指定时间取消任务
cts.CancelAfter(1000);

在运行任务前,传入 Token 属性

var response = await client.GetAsync(req.Url, cts.Token);

// 当任务被取消时,会引发 OperationCanceledException 异常

完整代码

private async void OnTaskBasedAsyncPattern(object sender, RoutedEventArgs e)
{
  cts = new CancellationTokenSource();
  try
  {
    foreach (var req in GetSearchRequests())
    {
      var clientHandler = new HttpClientHandler
      {
         Credentials = req.Credentials
      };
      var client = new HttpClient(clientHandler);

        // 使用 GetAsync 发出异步请求
      var response = await client.GetAsync(req.Url, cts.Token);
        // 异步 返回字符串格式的内容
      string resp = await response.Content.ReadAsStringAsync();

      // 解析XML 可能需要一段时间,用 Task.Run 同步功能创建后台任务
      await Task.Run(() =>
      {
        var images = req.Parse(resp);
        foreach (var image in images)
        {
          cts.Token.ThrowIfCancellationRequested();
          searchInfo.List.Add(image);
        }
      }, cts.Token);
    }
  }
  catch (OperationCanceledException ex)
  {
    MessageBox.Show(ex.Message);
  }
}

取消自定义任务

await Task.Run(() =>
{
  var images = req.Parse(resp);
  foreach (var image in images)
  {
    cts.Token.ThrowIfCancellationRequested();
    searchInfo.List.Add(image);
  }
}, cts.Token);

利用 Task.Run 传递参数进去。但是对于自定义任务,需要检查是否请求取消操作,可以用 cts.Token.IsCancellationRequested 属性。在抛出异常前,如果需要做一些清理工作,最好验证一下,是否请求取消操作。如果不需要做清理工作,检查之后,会立即用 ThrowIfCancellationRequested 方法触发异常。

 

 

posted @ 2016-09-17 22:46  笨重的石头  阅读(449)  评论(0编辑  收藏  举报