关于 async 和 await 两个关键字(C#)【并发编程系列_5】

关于,async,await,两个,关键字,c#,并发,编程,系列 · 浏览次数 : 1172

小编点评

文章介绍了 async await 的使用场景,包括 I/O 密集型的异步操作。文章使用了例子来说明了 async await 的用法。 **文章中涉及到的关键概念:** * async await * I/O 密集 * Task * Task.GetAwaiter() * awaitable *委托 * GetResult() **文章的重点:** * async await 特别适合 I/O 密集型的异步操作。 * I/O 密集是指当多个线程需要进行 I/O 操作时,可以使用 I/O 密集来优化代码。 * Task.GetAwaiter() 方法可以返回一个 awaitable 的对象,用于在主线程中等待异步操作完成。 * awaitable 是一个继承了 INotifyCompletion.OnCompleted 方法的接口。 * 通过 GetResult() 方法可以阻塞主线程,直到异步操作完成。 **文章的结论:** async await 是一个非常重要的异步编程技术,可以用于优化 I/O 密集型的异步操作。文章通过例子展示了 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 方法中,调用一个 async 修饰的异步方法,由于没有使用 await 修饰,所以已同步方式运行。

public class Program
{
    // static void Main(string[] args) // 由于在 .Net 7.1 直线 Main 方法不支持 async,所以只能通过 AsyncTask() 来调用异步方法
    public static async Task Main() // 更新至 .Net 7.1 或更高版本即可用异步 Main() 方法,若其中没有用到 await 关键字,会出现警告:CS1998:此异步方法缺少"await"运算符,将以同步方式运行。。。
    {
        ConsoleExt.WriteLine("--开始!");
        ConsoleExt.WriteLine($"--下面我(主线程)先通知下儿子(子线程)也开始。 ");
        // 调用 async 修饰的方法,也就是异步执行的方法
        AsyncTask(); // 异步方法,不占用主线程,是另新创建的新的子线程
        ConsoleExt.WriteLine("--我(主线程)已经让我儿子(子线程)开始工作了,我也继续工作");
        ConsoleExt.WriteLine($"--我(主线程)完成! ");
        Console.ReadLine();
    }
    // async 修饰的方法,也就是异步方法,不占用主线程
    public static async Task AsyncTask()
    {
        Thread.Sleep(1000);
        ConsoleExt.WriteLine($"--我刚到,还没找到儿子(子线程)的房间,");
        var result = await WasteTime(); // 主线程遇到 await,是不会等待的,直接继续执行,接下来的事情交给子线程
        ConsoleExt.WriteLine(result);
        ConsoleExt.WriteLine($"儿子(子线程)已经干完了应该干的事情! ");
    }
    // async 修饰的方法,也就是异步方法,不占用主线程
    private static async Task<string> WasteTime()
    {
        ConsoleExt.WriteLine($"--我终于找到了,下面准备让儿子(子线程)开干!");
        return await Task.Run(() => // 创建一个子线程
        {
            ConsoleExt.WriteLine($"儿子(子线程)开始异步执行了! ");
            // 模拟耗时操作
            Thread.Sleep(5000);
            return $"儿子(子线程)异步执行完了。";
        });
    }
}
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 加了双横杠--标识的内容是主线程的输出:

二、关于 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() 方法。

本文参考资料:https://learn.microsoft.com/zh-cn/dotnet/csharp/asynchronous-programming/task-asynchronous-programming-model    async & await 的前世今生(Updated)    C#进阶之Async await异步编程   

五、小小的总结

本文只是起到对于 async await 有个初步的理解作用,达到能看懂和会用的目的,而微软对于多线程的应用远不止于此,可以参考其他博友的文章、官方文档、专业书籍等等。

另外,async await 特别适合 I/O 密集型的异步操作,详细论证推荐参考: 理解 Task 和 async await

与关于 async 和 await 两个关键字(C#)【并发编程系列_5】相似的内容:

关于 async 和 await 两个关键字(C#)【并发编程系列_5】

本文只是起到对于 async await 有个初步的理解作用,达到能看懂和会用的目的,而微软对于多线程的应用远不止于此,可以参考其他博友的文章、官方文档、专业书籍等等。

【C#异步】异步多线程的本质,上下文流转和同步

引言 net同僚对于async和await的话题真的是经久不衰,这段时间又看到了关于这方面的讨论,最终也没有得出什么结论,其实要弄懂这个东西,并没有那么复杂,简单的从本质上来讲,就是一句话,async 和await异步的本质就是状态机+线程环境上下文的流转,由状态机向前推进执行,上下文进行环境切换,

关于async/await、promise和setTimeout执行顺序

前段时间领导给我们出了一道题,关于async/await、promise和setTimeout的执行顺序,网上查了查资料,这是头条的一道笔试题,记录一下,加深理解。 题目如下: async function async1() { console.log('async1 start'); await

关于Async、Await的一些知识点

在ASP.NET Core中,当一个HTTP请求到达服务器时,它会被分配给线程池中的一个线程来处理。该线程会执行相应的Controller方法。 如果这个方法是一个异步方法并且使用了await关键字,那么在await的代码执行完毕之前,这个线程会被释放回线程池,可以用来处理其他的HTTP请求。 当a

【JS】await异常捕获,这样做才完美

文章关注JavaScript中async/await的异常处理,指出未捕获异常的潜在风险。1)使用try-catch,虽全面但冗余;2)借助Promise的catch,减少冗余; 3) 利用await-to-js库简化异常处理

uni-app云开发入门

云函数 首先创建一个uniapp项目,创建项目时选择启用uniCloud云开发。 创建项目成功后,按照下面的步骤进行开发。 创建云函数 1.关联云服务器 2.创建云函数 一个云函数可以看成是一个后台接口 云函数实现 'use strict'; exports.main = async (event,

关于面向对象的方法并行执行的问题

LabVIEW的从同一个类实例化的多个对象如何执行各自的方法呢? 这几天跟同事讨论到LabVIEW的面向对象编程中,如果我设计的一个类有一个方法比较耗时,那么当我实例化多个对象时,那么这个耗时的方法是怎么执行的呢?是各自并行执行还是,必须等某一个对象的方法调用完,接下来调用第二个对象的该方法呢? 接

关于ComfyUI的一些Tips

关于ComfyUI的一些Tips 前言: 最近发的ComfyUI相关文章节奏不知道会不会很快,在创作的时候没有考虑很多,想着把自己的知识分享出去。后台也看到很多私信,有各种各样的问题,这是我欠缺考虑了,今天这篇文章呢,根据私信的问题我大致整理了一下,给大家一些小tips。 目录 一、将 ComfyU

关于领域驱动设计,大家都理解错了

翻遍整个互联网,我发现,关于领域驱动设计,大家都**理解错了**。 今天,我们尝试通过一篇文章的篇幅,给大家展示一个完全不同的视角,把“领域驱动设计”这六个字解释清楚。 ## 领域驱动设计学习资料现状 领域驱动设计的概念提出已经有20年的时间了,整个互联网充斥着大量书籍、文章和视频教程,这里我列举几

关于docker-compose up -d 出现超时情况处理

由于要搭建一个ctf平台,用docker一键搭建是出现超时情况 用了很多办法,换源,等之类的一样没办法,似乎它就是只能用官方那个一样很怪。 只能用一种笨办法来处理了,一个个pull。 打个比如: 打开相对应docker-compose.yml文件 可以看到image就是需要去下载的。那么此时你就可以