• 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.
     Result 1: {0}
     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.
     Result 1: {0}
     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.
     Result 1: {0}
     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 方法触发异常。

     

  • 相关阅读:
    2017年3月9日上午学习
    3.17上午
    3.16上午
    3.16下午
    3.15
    2017.3.14
    3.14
    217.3.13上午
    2017.4.7-morning
    2017.4.6-afternoon
  • 原文地址:https://www.cnblogs.com/z888/p/5879985.html
Copyright © 2020-2023  润新知