C#并发编程之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
{
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
{
//...
}
异步方法同步运行,直至到达其第一个 await 表达式,此时会将方法挂起,直到等待的任务完成。
如果 async 关键字修改的方法不包含 await 表达式或语句,则该方法将同步执行。编译器警告将通知你不包含 await 语句的任何异步方法,因为该情况可能表示存在错误。警告信息如下图:
异步方法可具有以下返回类型:
此异步方法既不能声明任何 in、ref 或 out 参数,也不能具有引用返回值,但它可以调用具有此类参数的方法。
三、关于 await 关键字
3.1 await 的用法示例
await 运算符(异步等待任务完成)可以让主线程,跳过对其所修饰的 async 方法的执行等待,将耗时操作交给子线程,从而完成异步操作。异步操作完成后,await 运算符将返回操作的结果(如果有)。
当 await 运算符用到表示已完成操作的异步方法时,它将立即返回操作的结果,类似于同步执行。
await 运算符不会阻止计算异步方法的线程。当 await 运算符占用子线程执行其异步方法时,主线程将返回到原执行路径上继续往下执行。
如下代码,两个 async 修饰的异步方法:
public static async Task Main()
{
Task
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
{
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
public class Program
{
static async Task Main(string[] args)
{
const int count = 5;
ConsoleExt.WriteLineAsync($"-------------------1开始示例异步测试");
//ConsoleExt.WriteLineAsync($"-------------------2开始示例异步测试");
//ConsoleExt.WriteLineAsync($"-------------------3开始示例异步测试");
// 创建一个新的任务,用于【生成】异步序列数据
IAsyncEnumerable
// 创建一个新的任务,用于【使用】异步序列数据
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
{
ConsoleExt.WriteLineAsync($"ConsumeAsyncSumSeqeunc 被调用");
await foreach (var value in sequence)
{
ConsoleExt.WriteLineAsync($"----接收延迟返回的值 {value}");
await Task.Delay(TimeSpan.FromSeconds(1)); // 模拟耗时操作
};
}
private static async IAsyncEnumerable
{
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 执行状态为 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 方法),通过方法,我们只是传递了一个委托(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的资料请关注脚本之家其它相关文章!
您可能感兴趣的文章:
- .NET Core系列之MemoryCache 初识
- 007手机一键Root(安机网一键Root) v3.0 官方最新版 一键ROOT您的Android手机
- 12306密码被盗了怎么办?12306密码外泄解决方法
- 12个字的qq网名
- 150M迷你型无线路由器怎么设置?
- 192.168.1.1打不开怎么办?路由器192.168.1.1打不开的原因以及解决办法
- 2011年电子报合订本 电子报 编辑部 中文 PDF版 [84M]
- 2015年1月15日小米新旗舰发布会现场图文直播
- 2016.3.1vivo Xplay5新品发布会现场视频直播 优酷直播
- 2016华为P9发布会视频直播地址 4月15日华为P9国行发布会直播