C#初识async与await

17 篇文章 11 订阅

一、引言

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();
        }

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

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


二、async与await

1. async概述

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

public async Task<int> ExampleMethodAsync()
{
    //...
}

官方文档原文中,接下来有这么一段话:
如果你是一个异步编程的新手,或不知道异步方法如何使用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[]>实例,它表示一个异步操作,该操作完成时会生成一个字节数组(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.

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

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

  • Task
  • Task<TResult>
  • ValueTask
  • ValueTask<TResult>

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

如果表达式t的类型是Task<TResult>或Value<TResult>,则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);

异步方法会以同步的方式运行,直到遇到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="&lt;Enter a URL&gt;" VerticalAlignment="Top" Width="310" Name="ResultsTextBox"/>

作为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<int>, which means that the method
    // eventually produces an int result. However, ExampleMethodAsync returns
    // the Task<int> 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

三、结尾

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

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

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

// 假设Fun2是一个很耗时的方法
async Task Fun1 ()
{
	await Task.Run(()=>{
		Fun2();
	});
	Fun3();
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值