• C#初识async与await


    一、引言

    C#中经常可以看到在一个方法前面有async修饰符。比如,最近在学习Quartz.NET,在教程中就有这样的写法:

    		private static async Task Main(string[] args)
            {
                // Grab the Scheduler instance from the Factory
                StdSchedulerFactory factory = new StdSchedulerFactory();
                IScheduler scheduler = await factory.GetScheduler();
    
                // and start it off
                await scheduler.Start();
    
                // some sleep to show what's happening
                await Task.Delay(TimeSpan.FromSeconds(10));
    
                // and last shut down the scheduler when you are ready to close your program
                await scheduler.Shutdown();
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    并且在async修饰的方法中,总会出现await的身影。所以你想抛开async和await中的某一个,去单独认识另一个是很难的。

    今天,花一篇博客的功夫来简单认识一下这两者。


    二、async与await

    1. async概述

    首先这里的async是一个关键字,同时也是修饰符(和abstract、static一样)。
    使用async修饰符可以将一个方法、lambda表达式或匿名方法指定为异步的。如果在方法或表达式上使用async修饰符,则它就被称为异步方法(async method)。下面示例代码定义了一个名为ExampleMethodAsync的异步方法:

    public async Task<int> ExampleMethodAsync()
    {
        //...
    }
    
    • 1
    • 2
    • 3
    • 4

    官方文档原文中,接下来有这么一段话:
    如果你是一个异步编程的新手,或不知道异步方法如何使用await操作符来执行可能长时间运行的工作,而不阻塞调用者的线程,请阅读使用async和await异步编程的介绍。

    显然,此时牵涉到了await,不过上面这篇文章太长,这边就不展开讲了,直接看await的简单描述。

    2. await概述

    await是一个operator(运算符或者操作符),该运算符会挂起(suspend)封闭的异步方法(async method),直到操作对象的异步操作完成。

    这句话初看比较难懂,稍微拆解一下。

    1. operator,运算符,跟加减乘除一样,作用于某个值(或对象),然后该值会进行一些运算,发生变化。
    2. 挂起,就是使某个过程暂停。
    3. 封闭的,我们可以想象方法(或者说函数)是一个容器,里面装载了一些运算的语句,随着运算的进行,方法(容器)中的状态会发生变化,此时我挂起方法,就相当于把方法(连同那些状态)封闭起来,不再改变。
    4. 异步方法,指的是该方法不是阻塞的,我运行到某个点,可能要等很久,此时我不等了,直接去干别的事情了,该点运行完之后通知我回来继续运行。
    5. 直到操作对象的异步操作完成,就是说await作用的对象的其他异步操作还在进行,进行完了我再回来继续执行await下面的语句。

    当异步操作完成时,await运算符返回运算的结果(如果有的话)。
    当await运算符应用于已完成运算的操作数(没有其他异步运算了)时,它会立即返回运算结果,而不会暂停封闭方法。await运算符不会阻塞运算异步方法的线程。当await运算符挂起封闭的异步方法时,控制权会返回到方法的调用方。

    下面画了个草图,因为用的代码绘图,有的地方有歧义,不是绝对准确的,但大致就是这个意思。

    1. 这里的OperationAsync是个多线程(不一定要多线程,只要是异步操作就行。不过用代码来演示的话,你想表现出await的效果,多开一个线程比较方便一些。因为去操控IO要写许多其他代码)的异步操作。
    2. 蓝色手描实线表示该方法正在执行。
      异步≠多线程,但异步往往会和多线程一起用。
      在这里插入图片描述

    3. await代码示例

    下面是官方代码示例,
    HttpClient.GetByteArrayAsync方法返回Task实例,它表示一个异步操作,该操作完成时会生成一个字节数组(byte[])。
    在操作完成之前,await操作符会挂起DownloadDocsMainPageAsync方法。
    当DownloadDocsMainPageAsync挂起时,控制权会返回给Main方法,Main方法是DownloadDocsMainPageAsync的调用者。
    Main方法一直执行,直到它需要DownloadDocsMainPageAsync方法执行的异步操作的结果。
    当GetByteArrayAsync获得所有字节时,DownloadDocsMainPageAsync方法的其余部分继续执行。在这之后,Main方法的剩余部分继续执行。

    using System;
    using System.Net.Http;
    using System.Threading.Tasks;
    
    public class AwaitOperator
    {
        public static async Task Main()
        {
            Task<int> downloading = DownloadDocsMainPageAsync();
            Console.WriteLine($"{nameof(Main)}: Launched downloading.");
    
            int bytesLoaded = await downloading;
            Console.WriteLine($"{nameof(Main)}: Downloaded {bytesLoaded} bytes.");
        }
    
        private static async Task<int> DownloadDocsMainPageAsync()
        {
            Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: About to start downloading.");
    
            var client = new HttpClient();
            byte[] content = await client.GetByteArrayAsync("https://docs.microsoft.com/en-us/");
    
            Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: Finished downloading.");
            return content.Length;
        }
    }
    // Output similar to:
    // DownloadDocsMainPageAsync: About to start downloading.
    // Main: Launched downloading.
    // DownloadDocsMainPageAsync: Finished downloading.
    // Main: Downloaded 27700 bytes.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    只能在async关键字修饰的方法、lambda表达式或匿名方法上使用await运算符。在异步方法中,你不能在同步函数体中、锁的语句块中和不安全的上下文中使用await运算符。

    await运算符的操作对象通常是以下几种.NET类型之一:

    • Task
    • Task
    • ValueTask
    • ValueTask

    不过,任何可等待(awaitable)的表达式都可以是await运算符的作用对象。

    如果表达式t的类型是Task或Value,则await t的类型是TResult。
    如果t的类型是Task或ValueTask,则await t的类型是void。
    这两种情况下,若t抛出异常,则await t重新抛出异常。
    简单讲就是,表达式带参数返回参数类型,不带参数返回空,抛出异常就抛出异常。

    小结
    那么在两个小节的简单介绍之后,用一句话来总结一下await。
    await运算符,会等待操作对象的异步操作完成,并会挂起函数,让出执行权。

    4. async代码示例

    在对await有个大概理解后,继续学习async关键字。
    下面代码是在一个异步方法中的,并且它调用了 HttpClient.GetStringAsync方法:

    string contents = await httpClient.GetStringAsync(requestUrl);
    
    • 1

    异步方法会以同步的方式运行,直到遇到await表达式,此时该方法被挂起,直到等待的任务完成。与此同时,控制(执行)权返回给方法的调用者。

    这段描述和await中描述的一样。

    如果async关键字修饰的方法不包含await表达式或语句,则该方法将同步执行。编译器会警告你该异步方法不包含await语句,因为这种情况可能会指示错误。详情看编译器警告CS4014。

    async关键字是上下文相关的,因为它只有在修饰方法、lambda表达式或匿名方法时才是关键字。在其它场合下,它被解释成标识符。

    下面正式看一个例子,该示例展示了异步事件处理器StartButton_Click和异步方法ExampleMethodAsync之间的控制结构与流程。该异步方法的执行结果是网页的字符数。该代码适用于WPF程序。

    在WPF中运行它,你需要一个名为StartButton的按钮控件和一个名为ResultsTextBox的文本框控件。不要忘了给它们设置名称(name)和处理器(handler),就像下面这样:

    <Button Content="Button" HorizontalAlignment="Left" Margin="88,77,0,0" VerticalAlignment="Top" Width="75"
            Click="StartButton_Click" Name="StartButton"/>
    <TextBox HorizontalAlignment="Left" Height="137" Margin="88,140,0,0" TextWrapping="Wrap"
             Text="<Enter a URL>" VerticalAlignment="Top" Width="310" Name="ResultsTextBox"/>
    
    • 1
    • 2
    • 3
    • 4

    作为WPF程序运行:

    • 将代码粘贴到MainWindow.xaml.cs中的MainWindow类中。
    • 添加System.Net.Http引用。
    • 为System.Net.Http添加using指令。
    private async void StartButton_Click(object sender, RoutedEventArgs e)
    {
        // ExampleMethodAsync returns a Task, which means that the method
        // eventually produces an int result. However, ExampleMethodAsync returns
        // the Task value as soon as it reaches an await.
        ResultsTextBox.Text += "\n";
    
        try
        {
            int length = await ExampleMethodAsync();
            // Note that you could put "await ExampleMethodAsync()" in the next line where
            // "length" is, but due to when '+=' fetches the value of ResultsTextBox, you
            // would not see the global side effect of ExampleMethodAsync setting the text.
            ResultsTextBox.Text += String.Format("Length: {0:N0}\n", length);
        }
        catch (Exception)
        {
            // Process the exception if one occurs.
        }
    }
    
    public async Task<int> ExampleMethodAsync()
    {
        var httpClient = new HttpClient();
        int exampleInt = (await httpClient.GetStringAsync("http://msdn.microsoft.com")).Length;
        ResultsTextBox.Text += "Preparing to finish ExampleMethodAsync.\n";
        // After the following return statement, any method that's awaiting
        // ExampleMethodAsync (in this case, StartButton_Click) can get the
        // integer result.
        return exampleInt;
    }
    // The example displays the following output:
    // Preparing to finish ExampleMethodAsync.
    // Length: 53292
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    三、结尾

    说实话,通过这篇文章想对这对组合的应用场景有较深认识是做不到的。
    在学习这对关键字前,最好对同步异步有个认识。
    这里的异步操作放应用程序编程里,我觉得大部分时候都是一个多线程的任务,除非你会去操作一些IO。

    归纳
    1️⃣async和await往往一起出现
    2️⃣async修饰符,指明该方法是一个异步方法
    3️⃣await运算符,等候操作对象的异步操作完成

    目前来看,这对组合用于控制执行顺序非常有用,比如:

    // 假设Fun2是一个很耗时的方法
    async Task Fun1 ()
    {
    	await Task.Run(()=>{
    		Fun2();
    	});
    	Fun3();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果没有await和Task.Run,那运行至Fun2,可能就卡很久,再运行Fun3。
    如果没有await,只有Task.Run,那Fun2和Fun3执行顺序不一定。
    如果有await和Task.Run,则运行至Fun2,执行权转移,等Fun2执行完再执行Fun3。

  • 相关阅读:
    微信小程序复杂对象的双向绑定(附代码可直接使用)
    代码随想录二刷day44
    笔试强训(二)
    maven命令上传文件到私服deploy-file
    初探C++ CRTP(奇异的递归模板模式)
    手把手教你用C语言制作七夕流星雨---优雅永不过时(详细教程)
    AcWing 1137. 选择最佳线路 题解(最短路)
    反射型XSS实验(2)
    第二章计算机网络参考模型
    考研数据结构与算法(六)树与二叉树
  • 原文地址:https://blog.csdn.net/BadAyase/article/details/126559085