C# 我是如何理解async await的
引言
写这篇文章的原因是网上关于C#
async
关键字和await
关键字的文章很多,但是在我看来写的有些绕了,其实这俩关键字很容易理解,需要注意的是:这和ES7中的async、await有很大区别
,如果你理所当然的用ES7中的理解,那么你已经走歪了。
理解设计初衷
async/await
关键字设计的初衷
:
是为了
让Task.ContinueWith之后,或嵌套Task之后,return新的Task的代码
看起来更简洁
。
并不是为了单个Task
的情况下省去Start/Run或task.Result的调用
。
下面写了2个小例子,非常简单的代码,当你理解这两个关键字出现的原因后,那么你就更容易理解为什么使用,在什么时候使用。
我们先看下面的代码
using System;
using System.Threading.Tasks;
namespace Test
{
class Program
{
static Task<long> SquareTask(long i)
{
Task<long> task = Task.Run(() => {
return i * i;
});
return task;
}
static Task<long> FourthPowerTask(long i)
{
Task<long> task = SquareTask(i);
Task<long> continueTask = task.ContinueWith((preTask) =>
{
//假设我们保存之前计算的平方(preTask.Result) 进数据库(模拟会用到之前task的返回值)
Console.WriteLine("存入数据库 {0}", preTask.Result);
//此时我们返回再计算一次的平方
return preTask.Result * preTask.Result;
});
return continueTask;
}
static void Main(string[] args)
{
Task<long> task = FourthPowerTask(10);
Console.WriteLine("4次方结果 {0}", task.Result);
}
}
}
代码中
FourthPowerTask
方法创建了一个Task
,并且让它先将平方值存入数据库,然后继续计算
之前Task结果的平方
,并继续返回新的Task
。
注意:代码中我们只嵌套了一层ContinueWith
,如果Task
里面嵌套Task
变得越来越多,代码的可读性就变得更差。
使用async await让代码看起来简洁
await
:表示等待一个异步方法的结果
,但必须和async
同时出现。async
:表示当前方法是异步方法
,但必须和await
同时出现。- 方法内
await在Task对象之前
,表示需要获取Task.Result
,此时当前方法
必须使用async
关键字。
下面的代码使用只是为了让ContinueWith后返回新的Task
的代码,变的简洁
(设计初衷)。
在下面的代码中你会发现,我们之前看似通过一层一层回调处理的代码变成了最原始的,函数式代码。
using System;
using System.Threading.Tasks;
using System.Threading;
namespace Test
{
class Program
{
static Task<long> SquareTask(long i)
{
Task<long> task = Task.Run(() => {
return i * i;
});
return task;
}
static async Task<long> FourthPowerTask(long i)
{
var result = await SquareTask(i);
//假设我们保存之前计算的平方 进数据库(模拟会用到之前task的返回值)
Console.WriteLine("存入数据库 {0}", result);
//此时我们返回再计算一次的平方
//async关键字会将返回值封装为Task
return result * result;
}
static void Main(string[] args)
{
Task<long> task = FourthPowerTask(10);
Console.WriteLine("4次方结果 {0}", task.Result);
}
}
}
死锁可能性
探究死锁的原因我们先要分清楚线程是怎么处理asyn和await的。
我们将上面的代码改一改,看看线程ID
using System;
using System.Threading.Tasks;
using System.Threading;
namespace Test
{
class Program
{
static Task<long> SquareTask(long i)
{
Console.WriteLine("SquareTask 进入线程:{0}", Thread.CurrentThread.ManagedThreadId);
Task<long> task = Task.Run(() => {
Console.WriteLine("SquareTask 执行线程:{0}", Thread.CurrentThread.ManagedThreadId);
return i * i;
});
Console.WriteLine("SquareTask 返回线程:{0}", Thread.CurrentThread.ManagedThreadId);
return task;
}
static async Task<long> FourthPowerTask(long i)
{
Console.WriteLine("FourthPowerTask 进入线程:{0}", Thread.CurrentThread.ManagedThreadId);
var result = await SquareTask(i);
//假设我们保存之前计算的平方 进数据库(模拟会用到之前task的返回值)
Console.WriteLine("存入数据库 {0}", result);
//此时我们返回再计算一次的平方
//async关键字会将返回值封装为Task
Console.WriteLine("FourthPowerTask 返回线程:{0}", Thread.CurrentThread.ManagedThreadId);
return result * result;
}
static void Main(string[] args)
{
Console.WriteLine("Main 执行线程:{0}", Thread.CurrentThread.ManagedThreadId);
Task<long> task = FourthPowerTask(10);
Console.WriteLine("4次方结果 {0}", task.Result);
}
}
}
输出
Main 执行线程:1
FourthPowerTask 进入线程:1
SquareTask 进入线程:1
SquareTask 返回线程:1
SquareTask 执行线程:4
存入数据库 100
FourthPowerTask 返回线程:4
4次方结果 10000
- 发现在
await之前
的代码,都是在(调用者线程)主线程执行
的await关键字之后
的代码,是在另外一个线程执行
的,这和ContinueWith
是不是很像?- 但是
有例外
,看下面。
需要注意
的是,在WPF/WinForm
程序或asp.net
中,某些情况下await之后的代码还是在主线程执行
,它会还原context
到调用者的context
,也就是说如果在await之后
,你调用了Wait
或Result
阻塞,调用者
那边也在等Result
,此时就会产生死锁,我们可以调用task.ConfigureAwait(false)
,让await之后
的代码不要恢复
到调用者的context
执行。
死锁主要发生在
1、WPF/WinForm程序
2、asp.net (不包括asp.net core)程序
写在最后
当你确定使用async/await
时,应该考虑的是否会形成一个Task的调用链(当前方法需要使用之前Task的值,并且要返回新的Task),而不是完全不思考的情况下使用这两个关键字。
代码是写给人看的,给机器看那是编译器和解释器的事,在你足够了解底层,对编程有自己的理解后,你写的代码才能兼顾性能和可读性。