C#并发编程之async和await关键字详解

 更新时间:2023年07月25日 10:23:51   作者:橙子家  
对于 async 和 await 两个关键字,对于一线开发人员再熟悉不过了,到处都是它们的身影,下面小编就来和大家记录汇总下它们的使用吧

〇、前言

对于 async 和 await 两个关键字,对于一线开发人员再熟悉不过了,到处都是它们的身影。

从 C# 5.0 时代引入 async 和 await 关键字,我们使用 async 修饰符可将方法、lambda 表达式或匿名方法指定为异步。 如果对方法或表达式使用此修饰符,则其称为异步方法。async 和 await 通过与 .NET Framework 4.0 时引入的任务并行库(TPL:Task Parallel Library)构成了新的异步编程模型,即 TAP(基于任务的异步模式 Task-based asynchronous pattern)。

但是如果对他们不太了解的话,会有很多麻烦出现,所以最近查了一些资料,也看了几个大佬的介绍,今天来记录汇总下。

一、先通过一个简单的示例来互相认识下

如下代码,在 Main 方法中,调用一个异步方法,因为 Main 本身不支持 async,所以不能直接使用 await 关键字来完成异步等待等操作。

static void Main(string[] args) // 由于 Main 方法不支持 async,所以只能通过 AsyncTask() 来调用异步方法
{
    Console.WriteLine("--开始!");
    Console.WriteLine($"--下面我(主线程)先通知下儿子(子线程)也开始。 我的 ID:{Thread.CurrentThread.ManagedThreadId}");
    // 调用 async 修饰的方法,也就是异步执行的方法
    AsyncTask(); // 异步方法,不占用主线程,是另新创建的新的子线程
    Console.WriteLine("--我(主线程)已经让我儿子(子线程)开始工作了,我也继续工作");
    Console.WriteLine($"--我(主线程)完成! 我的 ID:{Thread.CurrentThread.ManagedThreadId}");
    Console.ReadLine();
}
// async 修饰的方法,也就是异步方法,不占用主线程
public static async Task AsyncTask()
{
    Thread.Sleep(1000);
    Console.WriteLine($"--我刚到,还没找到儿子(子线程)的房间,我的 ID:{Thread.CurrentThread.ManagedThreadId}");
    var result = await WasteTime(); // 主线程遇到 await,是不会等待的,直接继续执行,接下来的事情交给子线程
    Console.WriteLine(result);
    Console.WriteLine($"儿子(子线程)已经干完了应该干的事情! 我的 ID:{Thread.CurrentThread.ManagedThreadId}");
}
// async 修饰的方法,也就是异步方法,不占用主线程
private static async Task<string> WasteTime()
{
    Console.WriteLine($"--我终于找到了,下面准备让儿子(子线程)开干!我的 ID:{Thread.CurrentThread.ManagedThreadId}");
    return await Task.Run(() => // 创建一个子线程
    {
        Console.WriteLine($"儿子(子线程)开始异步执行了! 我的 ID:{Thread.CurrentThread.ManagedThreadId}");
        // 模拟耗时操作
        Thread.Sleep(5000);
        return $"儿子(子线程)异步执行完了。我的 ID:{Thread.CurrentThread.ManagedThreadId}";
    });
}

如下结果输出,加了双横杠--的是主线程的输出:

二、关于 async 关键字

使用 async 修饰符可将方法、lambda 表达式或匿名方法指定为异步,此时 async 称为关键字,其他所有上下文中都解释为标识符。如果对方法或表达式使用此修饰符,则其称为异步方法。如下代码,定义一个异步方法 ExampleMethodAsync():

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

异步方法同步运行,直至到达其第一个 await 表达式,此时会将方法挂起,直到等待的任务完成。

如果 async 关键字修改的方法不包含 await 表达式或语句,则该方法将同步执行。编译器警告将通知你不包含 await 语句的任何异步方法,因为该情况可能表示存在错误。警告信息如下图: 

异步方法可具有以下返回类型:

  • Task
  • Task<TResult>
  • void。 对于除事件处理程序以外的代码,通常不鼓励使用 async void 方法,因为调用方不能 await 那些方法,并且必须实现不同的机制来报告成功完成或错误条件。
  • 任何具有可访问的 GetAwaiter 方法的类型。 System.Threading.Tasks.ValueTask<TResult> 类型属于此类实现。 它通过添加 NuGet 包 System.Threading.Tasks.Extensions 的方式可用。

此异步方法既不能声明任何 in、ref 或 out 参数,也不能具有引用返回值,但它可以调用具有此类参数的方法。

三、关于 await 关键字

3.1 await 的用法示例

await 运算符(异步等待任务完成)可以让主线程,跳过对其所修饰的 async 方法的执行等待,将耗时操作交给子线程,从而完成异步操作。异步操作完成后,await 运算符将返回操作的结果(如果有)。

当 await 运算符用到表示已完成操作的异步方法时,它将立即返回操作的结果,类似于同步执行。

await 运算符不会阻止计算异步方法的线程。当 await 运算符占用子线程执行其异步方法时,主线程将返回到原执行路径上继续往下执行。

如下代码,两个 async 修饰的异步方法:

  • 首先在【1】位置调用异步方法DownloadDocsMainPageAsync(),由于这里没有 await 运算符,所以按照同步方式运行,进入到方法体内部,到达【2】位置。
  • 在【2】位置,代码中通过在异步方法GetByteArrayAsync()前加了 await 运算符,预示着这里将进行异步操作,创建新的线程,然后释放主线程,继续回Main()函数中往下运行。
  • 由于【2】这一行代码是耗时操作,因此主线程执行到【3】位置,这里有出现了 await 运算符,指的是等待异步线程的结果,此时主线程就下线了,接下来就是子线程的表演时间了。
  • 最后子线程下载操作完成,返回到【3】位置,完成其余的工作。
public static async Task Main()
{
    Task<int> downloading = DownloadDocsMainPageAsync(); // 【1】
    Console.WriteLine($"{nameof(Main)}: 启动下载。。。ThreadID:{Thread.CurrentThread.ManagedThreadId}");
    int bytesLoaded = await downloading; // 【3】
    Console.WriteLine($"{nameof(Main)}: 共下载了 {bytesLoaded} bytes。ThreadID:{Thread.CurrentThread.ManagedThreadId}");
    Console.ReadLine();
}
private static async Task<int> DownloadDocsMainPageAsync()
{
    Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: 即将开始下载。ThreadID:{Thread.CurrentThread.ManagedThreadId}");
    var client = new HttpClient();
    byte[] content = await client.GetByteArrayAsync("https://learn.microsoft.com/en-us/"); // 【2】
    Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: 完成下载。ThreadID:{Thread.CurrentThread.ManagedThreadId}");
    return content.Length;
}

输出结果如下图:

代码实际执行的流程大概画下:

3.2 await foreach() 示例

可以通过 await foreach 语句来使用异步数据流,即实现 IAsyncEnumerable<T> 接口的集合类型。异步检索下一个元素时,可能会挂起循环的每次迭代。

public class Program
{
    static async Task Main(string[] args)
    {
        const int count = 5;
        ConsoleExt.WriteLineAsync($"-------------------1开始示例异步测试");
        //ConsoleExt.WriteLineAsync($"-------------------2开始示例异步测试");
        //ConsoleExt.WriteLineAsync($"-------------------3开始示例异步测试");
        // 创建一个新的任务,用于【生成】异步序列数据
        IAsyncEnumerable<int> pullBasedAsyncSequence = ProduceAsyncSumSeqeunc(count);
        // 创建一个新的任务,用于【使用】异步序列数据
        var consumingTask = Task.Run(() => ConsumeAsyncSumSeqeunc(pullBasedAsyncSequence));
        ConsoleExt.WriteLineAsync($"-------------------开始做其他耗时操作");
        await Task.Delay(TimeSpan.FromSeconds(3)); // 模拟耗时操作
        ConsoleExt.WriteLineAsync($"-------------------结束做其他耗时操作");
        await consumingTask; // 等待异步任务完成
        ConsoleExt.WriteLineAsync($"-------------------结束示例异步测试");
        Console.ReadLine();
    }
    static async Task ConsumeAsyncSumSeqeunc(IAsyncEnumerable<int> sequence) // 使用
    {
        ConsoleExt.WriteLineAsync($"ConsumeAsyncSumSeqeunc 被调用");
        await foreach (var value in sequence)
        {
            ConsoleExt.WriteLineAsync($"----接收延迟返回的值 {value}");
            await Task.Delay(TimeSpan.FromSeconds(1)); // 模拟耗时操作
        };
    }
    private static async IAsyncEnumerable<int> ProduceAsyncSumSeqeunc(int count) // 生成
    {
        ConsoleExt.WriteLineAsync($"ProduceAsyncSumSeqeunc 被调用");
        var sum = 0;
        for (var i = 0; i <= count; i++)
        {
            sum = sum + i;
            await Task.Delay(TimeSpan.FromSeconds(0.5)); // 模拟耗时操作
            ConsoleExt.WriteLineAsync($"ProduceAsyncSumSeqeunc 返回 sum:{sum}");
            yield return sum; // yield 关键字表示延迟加载,将全部返回值一个一个返回
        }
    }
}
public static class ConsoleExt
{
    public static void WriteLine(object message)
    {
        Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
    }
    public static async void WriteLineAsync(object message)
    {
        await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} "));
    }
}

输出结果如下图,特别关注一下线程 12,它不仅在 foreach 迭代中执行任务,而且还抽空把Main()方法中的也执行了,这样就极大的发挥了多线程的好处,任务操作安排的满满的,避免浪费资源。

常规的 foreach() 方法,是单线程的,后一个操作必须在前一个操作完成后开始,这样对于多逻辑处理器的机器来说,就像是宰牛刀对付小鸡儿了。

详情可参考:聊一聊C# 8.0中的await foreach

3.3 关于 await using()

可以说 await using() 的使用是和 IAsyncDisposable 接口息息相关的。

IAsyncDisposable 接口,提供一种用于异步释放非托管资源的机制。与之对应的就是提供同步释放非托管资源机制的接口 IDisposable。提供此类及时释放机制,可使用户执行资源密集型释放操作,从而无需长时间占用 GUI 应用程序的主线程。同时更好的完善.NET异步编程的体验,IAsyncDisposable诞生了。

现在 .NET 的很多类库都已经同时支持了 IDisposable 和 IAsyncDisposable。而从使用者的角度来看,其实调用任何一个释放方法都能够达到释放资源的目的。就好比 DbContext 的 SaveChanges和 SaveChangesAsync。但是从未来的发展角度来看,IAsyncDisposable 会成使用的更加频繁。因为它应该能够优雅地处理托管资源,而不必担心死锁。而对于现在已有代码中实现了 IDisposable 的类,如果想要使用 IAsyncDisposable。建议您同时实现两个接口,已保证使用者在使用时,无论调用哪个接口都能达到效果,而达到兼容性的目的。

如下示例代码继承了 IAsyncDisposable 接口,然后就可以使用 await using 语法了:

// 【前提】先实现接口 IAsyncDisposable
public class ExampleClass : IAsyncDisposable
{
	private Stream _memoryStream = new MemoryStream();
	public ExampleClass()
	{	}
	public async ValueTask DisposeAsync()
	{
		await _memoryStream.DisposeAsync();
	}
}
// 【第一种】然后就可以使用 using 语法糖
await using var s = new ExampleClass()
{
	// 具体操作。。。
};
// 【第二种】优化 同样是对象 s 只存在于当前代码块
await using var s = new ExampleClass();
// 具体操作。。。

详情可参考:熟悉而陌生的新朋友——IAsyncDisposable

四、await Task 和 Task.GetAwaiter()

4.1 关于 Task.GetAwaiter()

最常用的等待异步线程完成的修饰符就是 await,那么如果不用它怎么判断任务执行情况呢?这时候 Task.GetAwaiter() 就上场了。

如下代码,task.GetAwaiter().OnCompleted(() => { })的目的就是在 task 执行状态为 RunToCompletion 时执行其中的匿名函数。

class Program
{
    static void Main()
    {
        var task = Task.Run(() => {
            return GetName();
        });
        task.GetAwaiter().OnCompleted(() => {
            var name = task.Result;
            ConsoleExt.WriteLine("获取到的名称为:" + name);
        });
        ConsoleExt.WriteLine("主线程执行完毕");
        Console.ReadLine();
    }
    static string GetName()
    {
        ConsoleExt.WriteLine("另外一个线程在获取名称");
        Thread.Sleep(2000);
        return "GetName--名称";
    }
}
public static class ConsoleExt
{
    public static void WriteLine(object message)
    {
        Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
    }
    public static async void WriteLineAsync(object message)
    {
        await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} "));
    }
}

如下输出结果,1 为主线程,4 为子线程:

4.2 await Task 和 Task.GetAwaiter() 的区别

在异步返回的 Task 实例前加上 await 关键字之后,后面的代码会被挂起等待,直到 task 执行完毕有返回值的时候才会继续向下执行,这一段时间主线程会处于挂起状态。例如本文 3.1 await 的用法示例 中的示例,总共下载了多少内容在最后才被输出。

GetAwaiter() 方法则会返回一个 awaitable 的对象(继承了 INotifyCompletion.OnCompleted 方法),通过public void OnCompleted(Action continuation)方法,我们只是传递了一个委托(Action)进去,等 task 完成了就会执行这个委托,但是并不会影响主线程,下面的代码会立即执行。这也是为什么我们在本文上一章节 4.1 关于 Task.GetAwaiter() 的输出结果里面,“主线程执行完毕”写在最后,而非最后输出的原因。

那么我们通过 GetAwaiter() 方法如何能达到 await Task 的效果呢?

// GetResult() 方法就是阻塞线程,直到 task 执行完成,返回结果 name
var name = task.GetAwaiter().GetResult();
// 上边这行的效果,等同于
var name = await task;

await 实质是在调用 awaitable 对象的 GetResult() 方法。

以上就是C#并发编程之async和await关键字详解的详细内容,更多关于C# async await的资料请关注脚本之家其它相关文章!

相关文章

  • 深入理解C#中的扩展方法

    深入理解C#中的扩展方法

    下面这篇文章主要给大家介绍了关于c#中扩展方法的相关资料,文中通过示例代码介绍的非常详细,供大家学习参考,感兴趣的朋友可以了解下
    2020-06-06
  • C# 和 Python 的 hash_md5加密方法

    C# 和 Python 的 hash_md5加密方法

    这篇文章主要介绍了C# 和 Python 的 hash_md5加密方法,文章围绕着C# 和 Python 的 hash_md5加密的相关资料展开文章的详细呢偶然,需要的朋友可以参考一下,希望对你有所帮助
    2021-11-11
  • C#集合之自定义集合类

    C#集合之自定义集合类

    这篇文章介绍了C#集合之自定义集合类,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-05-05
  • 使用 C# 下载文件的多种方法小结

    使用 C# 下载文件的多种方法小结

    本文从最简单的下载方式开始步步递进,讲述了文件下载过程中的常见问题并给出了解决方案。并展示了如何使用多线程提升 HTTP 的下载速度以及调用 aria2 实现非 HTTP 协议的文件下载,对C# 下载文件相关知识感兴趣的朋友一起看看吧
    2021-08-08
  • C#数组的常用操作方法小结

    C#数组的常用操作方法小结

    Array数组在C#中同样是最基本的数据结构,下面为大家C#数组的常用操作方法小结,皆为细小的代码段,欢迎收看收藏
    2016-05-05
  • C#中实现查找mysql的安装路径

    C#中实现查找mysql的安装路径

    这篇文章主要介绍了C#中实现查找mysql的安装路径,本文讲解使用SQL语句查询出mysql的安装路径,方便在备份时使用,需要的朋友可以参考下
    2015-06-06
  • C#图表算法之最短路径

    C#图表算法之最短路径

    本文详细讲解了C#图表算法之最短路径,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-04-04
  • 基于C#实现电脑系统挂机锁

    基于C#实现电脑系统挂机锁

    这篇文章主要为大家详细介绍了如何利用C#实现电脑系统挂机锁,文中的示例代码讲解详细,对我们学习C#有一定的帮助,感兴趣的小伙伴可以跟随小编一起了解一下
    2022-12-12
  • C#异步编程之async/await详解

    C#异步编程之async/await详解

    异步这个概念在不同语境下有不同的解释,不同的编程语言有不同异步编程方法,在C#语言中,常常使用async/await等关键字,和Task等类来实现异步编程。本文就来和大家聊聊async与await吧
    2023-03-03
  • C#中的数组用法详解

    C#中的数组用法详解

    本文详细讲解了C#中的数组用法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-04-04

最新评论