c# 高级编程 15章309页 【什么是异步编程】【历史上 三种不同模式的 异步编程】

222 阅读5分钟

简介

  • .NET Framework 4.5任务并行库Task Parallel Library, TPL)添加到.NET中,使并行编程 更加容易
  • C# 5.0 增加了 两个关键字 来简化 异步编程asyncawait

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

三种 不同模式的 异步编程

第一种:异步模式

  • 是处理异步问题的第一种方式
  • 从.NET Framework 1.0开始,就提供了的异步功能
  • .NET Framework的许多类都实现了一个或多个异步模式
  • 定义了BeginXXX方法和EndXXX方法:
    • BeginXXX方法:
      • 输入参数
        • 接受其同步方法的所有输入参数
        • 还可以接受一个AsyncCallback委托类型的输入参数
          • 用于指定,异步方法执行结束后,需要做的事情
          • 委托类型AsyncCallback定义为:入参为IAsyncResult类型, 返回值为void
          • 具体实现为一个回调方法
          • 这个回调方法,不在调用线程中运行,而是在异步方法执行的线程中运行,因此,如果调用线程是UI线程,则在这个回调方法不在UI线程中运行也就无法访问UI上的元素 (试图访问UI元素的时候,会抛出异常:The calling thread cannot access this object because a different thread owns it)
      • 返回类型IAsyncResult。用于验证调用是否已经完成,并一直等到方法的执行结束
    • EndXXX方法:
      • 输入参数:接受BeginXXX所有输出参数
      • 返回类型其同步方法的返回类型
    • 例如:同步方法DownloadString()的异步版本就是BeginDownloadString()EndDownloadString()
  • 委托类型是否支持这种异步模式
    • .NET Framework 支持
    • .NET Core 不支持。调用委托的这些方法时,会抛出一个异常。

第二种:基于事件的 异步模式

  • 又称 异步组件模式
  • 由于第一种方式更新UI非常复杂,所以.NET Framework 2.0推出了这第二种处理异步问题的方式,使得更新UI变得容易
  • 事件处理程序被拥有同步上下文的线程调用。在Windows Forms, WPF和UWP中,这就是UI线程。因此可以直接从事件处理程序中访问UI元素。

第三种:基于任务的 异步模式。它是利用asyncawait关键字来实现

  • .NET Framework 4.5 推出这第三种方式
  • 基于Task类的异步模式(TAP)
  • await关键字会解除调用线程的阻塞,转而去完成其他事情。异步方法在后台执行,当异步方法执行完成时,调用线程就可以从后台Task中获得结果,然后继续执行await后面的代码
  • async创建了一个状态机,类似于yield return语句
  • 一切都变简单了
    • 没有阻塞, 也不需要切换回调用线程。这些都是自动实现的。
    • 代码顺序也和惯用的同步编程一样
  • Main方法要使用async, 则需要使用C# 7.1及以上

举个栗子

using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;

namespace AsyncHistory
{
    class Program
    {
        private const string url = "http://www.cninnovation.com";
        static async Task Main()
        {
            SynchronizedAPI();
            AsynchronousPattern();
            EventBasedAsyncPattern();
            await TaskBasedAsyncPatternAsync();
            Console.ReadLine();
        }

        private static async Task TaskBasedAsyncPatternAsync()
        {
            Console.WriteLine(nameof(TaskBasedAsyncPatternAsync));
            using (var client = new WebClient())
            {
                string content = await client.DownloadStringTaskAsync(url);
                Console.WriteLine(content.Substring(0, 100));
                Console.WriteLine();
            }
        }

        private static void EventBasedAsyncPattern()
        {
            Console.WriteLine(nameof(EventBasedAsyncPattern));
            using (var client = new WebClient())
            {
                client.DownloadStringCompleted += (sender, e) =>
                {
                    Console.WriteLine(e.Result.Substring(0, 100));
                };
                client.DownloadStringAsync(new Uri(url));
                Console.WriteLine();
            }
        }

        private static void AsynchronousPattern()
        {
            Console.WriteLine(nameof(AsynchronousPattern));
            WebRequest request = WebRequest.Create(url);
            IAsyncResult result = request.BeginGetResponse(ReadResponse, null);

            void ReadResponse(IAsyncResult ar)
            {
                using (WebResponse response = request.EndGetResponse(ar))
                {
                    Stream stream = response.GetResponseStream();
                    var reader = new StreamReader(stream);
                    string content = reader.ReadToEnd();
                    Console.WriteLine(content.Substring(0, 100));
                    Console.WriteLine();
                }
            }
        }

        private static void SynchronizedAPI()
        {
            Console.WriteLine(nameof(SynchronizedAPI));
            using (var client = new WebClient())
            {
                string content = client.DownloadString(url);
                Console.WriteLine(content.Substring(0, 100));
            }
            Console.WriteLine();
        }
    }
}

其中:同步调用

        private static void SynchronizedAPI()
        {
            using (var client = new WebClient())
            {
                string content = client.DownloadString(url);
                Console.WriteLine(content.Substring(0, 100));
            }
        }
  • WebClient类的同步API DownloadString()会阻塞调用线程

如果 用古早的第一种:异步模式

        private static void AsynchronousPattern()
        {
            WebRequest request = WebRequest.Create(url);
            IAsyncResult result = request.BeginGetResponse(ReadResponse, null);

            void ReadResponse(IAsyncResult ar)
            {
                using (WebResponse response = request.EndGetResponse(ar))
                {
                    Stream stream = response.GetResponseStream();
                    var reader = new StreamReader(stream);
                    string content = reader.ReadToEnd();
                    Console.WriteLine(content.Substring(0, 100));
                }
            }
        }
  • WebClient类没有支持 第一种古早异步模式的 API。这里为了举例,可以用WebRequest类来代替。
  • WebRequest类:
    • 它的BeginGetResponse()EndGetResponse(),使用了第一种古早的异步模式。是同步方法GetResponse()的异步版本
    • 它的Create()方法,创建WebRequest实例
    • BeginGetResponse()的输入参数是:
      • 委托类型AsyncCallback定义为:入参为IAsyncResult类型, 返回值为void

在这个例子中,BeginGetResponse()输入参数委托类型AsyncCallback具体实现为内嵌的本地函数:ReadResponse()方法:

  • 一旦异步调用完成,就会调用ReadResponse()方法
  • 由于本地函数的闭包功能,在本地函数内可以直接访问本地函数外声明的WebRequest类的实例request变量
  • 另外,假如ReadResponse()方法没有实现为本地函数,那么则需要将WebRequest类的实例request变量作为入参传进这个方法。这时,可以用IAysncResult类的AsyncState属性 得到 这个通过参数传入的 request变量

如果 用第二种: 基于事件的异步模式

        private static void EventBasedAsyncPattern()
        {
            using (var client = new WebClient())
            {
                //如下提前指定,事件处理程序
                //事件处理程序 由拥有同步上线文的线程来执行,在Windows Form, WPF, UWP程序中,那就是UI线程
                //所以,在此事件处理程序内,可以访问 UI元素
                client.DownloadStringCompleted += (sender, e) =>
                {
                    Console.WriteLine(e.Result.Substring(0, 100));
                };
                client.DownloadStringAsync(new Uri(url));
            }
        }

如果 用第三种:基于任务的异步模式

  • WebClient类提供了一个带后缀“Async”的方法,这个方法实现了基于Task的异步模式
        private static async Task TaskBasedAsyncPatternAsync()
        {
            using (var client = new WebClient())
            {
                string content = await client.DownloadStringTaskAsync(url);
                Console.WriteLine(content.Substring(0, 100));
            }
        }

思考 总结

对于这三种模式,有一件事需要 重点理解:

异步方法调用结束后,后续的方法 是由哪个线程来调用?

  • 对于第一种 古早的异步模式:是在执行异步方法的那个线程中调用
  • 对于第二种 基于事件的异步模式:是在拥有同步上下文的线程中调用
  • 对于第三种 基于任务的异步模式:默认是在拥有同步上下文的线程中调用,但是也可以配置。