zoukankan      html  css  js  c++  java
  • C# 异步编程

    ● Async Patterns(异步模式)
    ● Foundations(async和await关键字)
    ● ErrorHandling(异步方法的错误处理)

    异步编程的重要性

    使用异步编程,方法调用是在后台运行(通常在线程或任务的帮助下),并且不会阻塞调用线程。
    本章将学习3种不同模式的异步编程:异步模式、基于事件的异步模式和基于任务的异步模式(Task-based Asynchronous Pattern, TAP)。TAP是利用async和await关键字来实现的。通过这里的比较,将认识到异步编程新模式的真正优势。

    如果应用程序没有立刻响应用户的请求,会让用户反感。用鼠标操作,我们习惯了出现延迟,过去几十年都是这样操作的。有了触摸UI,应用程序要求立刻响应用户的请求。否则,用户就会不断重复同一个动作。

    因为在旧版本的.NET Framework中用异步编程非常不方便,所以并没有总是这样做。Visual Studio旧版本是经常阻塞UI线程的应用程序之一。例如,在Visual Studio 2010中,打开一个包含数百个项目的解决方案,这意味可能需要等待很长的时间。自从Visual Studio 2012以来,情况就不一样了,因为项目都是在后台异步加载的,并且选中的项目会优先加载。Visual Studio 2015的一个最新改进是NuGet包管理器不再实现为模式对话框。新的NuGet包管理器可以异步加载包的信息,同时做其他工作。这是异步编程内置到Visual Studio 2015中带来的重要变化之一。

    很多.NET Framework的API都提供了同步版本和异步版本。因为同步版本的API用起来更为简单,所以常常在不适合使用时也用了同步版本的API。在新的Windows运行库(WinRT)中,如果一个API调用时间超过40ms,就只能使用其异步版本。自从C# 5开始,异步编程和同步编程一样简单,所以用异步API应该不会有任何的障碍。

    异步编程的基础

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

    创建任务

    所有示例Foundations的代码都使用了如下依赖项和名称空间:
    依赖项: NETStandard.Library
    名称空间:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using static System.Console;
    

    下面从同步方法Greeting开始,该方法等待一段时间后,返回一个字符串:

    static string Greeting(string name)
        {
          Task.Delay(3000).Wait();
          return $"Hello, {name}";
        }
    

    定义方法GreetingAsync,可以使方法异步化。基于任务的异步模式指定,在异步方法名后加上Async后缀,并返回一个任务。异步方法GreetingAsync和同步方法Greeting具有相同的输入参数,但是它返回的是Task。Task定义了一个返回字符串的任务。一个比较简单的做法是用Task.Run方法返回一个任务。泛型版本的Task.Run()创建一个返回字符串的任务:

    static Task<string> GreetingAsync(string name)
    {
      return Task.Run<string>(() =>
      {
        return Greeting(name);
      });
    }
    

    调用异步方法

    可以使用await关键字来调用返回任务的异步方法GreetingAsync。使用await关键字需要有用async修饰符声明的方法。在GreetingAsync方法完成前,该方法内的其他代码不会继续执行。但是,启动CallerWithAsync方法的线程可以被重用。该线程没有阻塞:

    private async static void CallerWithAsync()
    {
      string result = await GreetingAsync("Stephanie");
      WriteLine(result);
    }
    

    如果异步方法的结果不传递给变量,也可以直接在参数中使用await关键字。在这里,GreetingAsync方法返回的结果将像前面的代码片段一样等待,但是这一次的结果会直接传给WriteLine方法:

    private async static void CallerWithAsync2()
    {
      WriteLine(await GreetingAsync("Stephanie"));
    }
    

    async修饰符只能用于返回.NET类型的Task或viod的方法,以及Windows运行库的IAsyncOperation。它不能用于程序的入口点,即Main方法不能使用async修饰符。await只能用于返回Task的方法。

    延续任务

    GreetingAsync方法返回一个Task对象。该Task对象包含任务创建的信息,并保存到任务完成。Task类的ContinueWith方法定义了任务完成后就调用的代码。指派给ContinueWith方法的委托接收将已完成的任务作为参数传入,使用Result属性可以访问任务返回的结果:

    private static void CallerWithContinuationTask()
    {
      Task<string> t1 = GreetingAsync("Stephanie");
      t1.ContinueWith(t =>
      {
        string result = t.Result;
        WriteLine(result);
      });
    }
    

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

    同步上下文

    如果验证方法中使用的线程,会发现CallerWithAsync方法和CallerWithContinuationTask方法,在方法的不同生命阶段使用了不同的线程。一个线程用于调用GreetingAsync方法,另外一个线程执行await关键字后面的代码,或者继续执行ContinueWith方法内的代码块。

    使用一个控制台应用程序,通常不会有什么问题。但是,必须保证在所有应该完成的后台任务完成之前,至少有一个前台线程仍然在运行。示例应用程序调用Console.ReadLine来保证主线程一直在运行,直到按下返回键。

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

    如果使用async和await关键字,当await完成之后,不需要进行任何特别处理,就能访问UI线程。默认情况下,生成的代码就会把线程转换到拥有同步上下文的线程中。WPF应用程序设置了DispatcherSynchronizationContext属性,Windows Forms应用程序设置了WindowsFormsSynchronization-Context属性。如果调用异步方法的线程分配给了同步上下文,await完成之后将继续执行。默认情况下,使用了同步上下文。如果不使用相同的同步上下文,则必须调用Task方法ConfigureAwait (continueOnCapturedContext:false)。例如,一个WPF应用程序,其await后面的代码没有用到任何的UI元素。在这种情况下,避免切换到同步上下文会执行得更快。

    使用多个异步方法

    在一个异步方法里,可以调用一个或多个异步方法。如何编写代码,取决于一个异步方法的结果是否依赖于另一个异步方法。

    按顺序调用异步方法

    使用await关键字可以调用每个异步方法。在有些情况下,如果一个异步方法依赖另一个异步方法的结果,await关键字就非常有用。在这里,GreetingAsync异步方法的第二次调用完全独立于其第一次调用的结果。这样,如果每个异步方法都不使用await,那么整个MultipleAsyncMethods异步方法将更快地返回结果,如下所示:

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

    使用组合器

    如果异步方法不依赖于其他异步方法,则每个异步方法都不使用await,而是把每个异步方法的返回结果赋值给Task变量,就会运行得更快。GreetingAsync方法返回Task。这些方法现在可以并行运行了。组合器可以帮助实现这一点。一个组合器可以接受多个同一类型的参数,并返回同一类型的值。多个同一类型的参数被组合成一个参数来传递。Task组合器接受多个Task对象作为参数,并返回一个Task。
    示例代码调用Task.WhenAll组合器方法,它可以等待,直到两个任务都完成。

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

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

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

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

    转换异步模式

    并非.NET Framework的所有类都引入了新的异步方法。在使用框架中的不同类时会发现,还有许多类只提供了BeginXXX方法和EndXXX方法的异步模式,没有提供基于任务的异步模式。但是,可以把异步模式转换为基于任务的异步模式。
    首先,从前面定义的同步方法Greeting中,借助于委托,创建一个异步方法。Greeting方法接收一个字符串作为参数,并返回一个字符串。因此,Func<string, string>委托的变量可用于引用Greeting方法。按照异步模式,BeginGreeting方法接收一个string参数、一个AsyncCallback参数和一个object参数,返回IAsyncResult。EndGreeting方法返回来自Greeting方法的结果——一个字符串——并接收一个IAsyncResult参数。这样,同步方法Greeting就通过一个委托变成异步方法。

    private Func<string, string> greetingInvoker = Greeting;
    
    private IAsyncResult BeginGreeting(string name, AsyncCallback callback, object state)
    {
      return greetingInvoker.BeginInvoke(name, callback, state);
    }
    
    private string EndGreeting(IAsyncResult ar)
    {
      return greetingInvoker.EndInvoke(ar);
    }
    

    现在,BeginGreeting方法和EndGreeting方法都是可用的,它们都应转换为使用async和await关键字来获取结果。TaskFactory类定义了FromAsync方法,它可以把使用异步模式的方法转换为基于任务的异步模式的方法(TAP)。

    示例代码中,Task类型的第一个泛型参数Task定义了调用方法的返回值类型。FromAsync方法的泛型参数定义了方法的输入类型。这样,输入类型又是字符串类型。FromAsync方法的前两个参数是委托类型,传入BeginGreeting和EndGreeting方法的地址。紧跟这两个参数后面的是输入参数和对象状态参数。因对象状态没有用到,所以给它分配null值。因为FromAsync方法返回Task类型,即示例代码中的Task,可以使用await,如下所示:

    private static async void ConvertingAsyncPattern()
    {
      string s = await Task<string>.Factory.FromAsync<string>(BeginGreeting, EndGreeting, "Angela", null);
      WriteLine(s);
    }
    

    取消

    在一些情况下,后台任务可能运行很长时间,取消任务就非常有用了。对于取消任务,.NET提供了一种标准的机制。这种机制可用于基于任务的异步模式。

    取消框架基于协助行为,不是强制性的。一个运行时间很长的任务需要检查自己是否被取消,在这种情况下,它的工作就是清理所有已打开的资源,并结束相关工作。

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

    开始取消任务

    首先,使用MainWindow类的私有字段成员定义一个CancellationTokenSource类型的变量cts。该成员用于取消任务,并将令牌传递给应取消的方法:

    public partial class MainWindow : Window
    {
      private SearchInfo _searchInfo = new SearchInfo();
      private object _lockList = new object();
      private CancellationTokenSource _cts;
      //. . .
    

    新添加一个按钮,用于取消正在运行的任务,添加事件处理程序OnCancel方法。在这个方法中,变量cts用Cancel方法取消任务:

    private void OnCancel(object sender, RoutedEventArgs e)
    {
      _cts? .Cancel();
    }
    

    CancellationTokenSource类还支持在指定时间后才取消任务。CancelAfter方法传入一个时间值,单位是毫秒,在该时间过后,就取消任务。

    使用框架特性取消任务

    现在,将CancellationToken传入异步方法。框架中的某些异步方法提供可以传入CancellationToken的重载版本,来支持取消任务。例如HttpClient类的GetAsync方法。除了URI字符串,重载的GetAsync方法还接受CancellationToken参数。可以使用Token属性检索CancellationTokenSource类的令牌。

    GetAsync方法的实现会定期检查是否应取消操作。如果取消,就清理资源,之后抛出OperationCanceledException异常。如下面的代码片段所示,catch处理程序捕获到了该异常:

    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);
        var response = await client.GetAsync(req.Url, _cts.Token);
        string resp = await response.Content.ReadAsStringAsync();
        //. . .
        }
      }
      catch (OperationCanceledException ex)
      {
        MessageBox.Show(ex.Message);
      }
    }
    

    取消自定义任务

    如何取消自定义任务?Task类的Run方法提供了重载版本,它也传递CancellationToken参数。但是,对于自定义任务,需要检查是否请求了取消操作。下例中,这是在foreach循环中实现的,可以使用IsCancellationRequsted属性检查令牌。在抛出异常之前,如果需要做一些清理工作,最好验证一下是否请求取消操作。如果不需要做清理工作,检查之后,会立即用ThrowIfCancellationRequested方法触发异常:

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

    现在,用户可以取消运行时间长的任务了。

  • 相关阅读:
    javaWeb普通类获取ApplicationContext
    医学图像处理最全综述
    图像分割最全综述
    解放双手——相机与IMU外参的在线标定
    从零开始一起学习SALM-ICP原理及应用
    SLAM、三维重建,语义相关数据集大全
    SLAM面试常见问题
    SLAM方向国内有哪些优秀公司?
    三维视觉、SLAM方向全球顶尖实验室汇总
    SLAM方向公众号、知乎、博客上有哪些大V可以关注?
  • 原文地址:https://www.cnblogs.com/chendeqiang/p/12861599.html
Copyright © 2011-2022 走看看