C#彻底搞懂asyncawait
1. 前⾔
Talk is cheap, Show you the code first!
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
AsyncMethod();
Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
}
private async Task AsyncMethod()
{
var ResultFromTimeConsumingMethod = TimeConsumingMethod();
string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
Console.WriteLine(Result);
//返回值是`Task`的函数可以不⽤`return`,或者将`Task`改为void
}
//这个函数就是⼀个耗时函数,可能是`IO`操作,也可能是`cpu`密集型⼯作。
private Task<string> TimeConsumingMethod()
{
var task = Task.Run(()=> {
Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
return "Hello I am TimeConsumingMethod";
});
return task;
}
我靠,这么复杂竟然有三个函数竟然有那么多⾏
别着急,慢慢看完,最后的时候你会发现使⽤async/await真的炒鸡优雅。
2. 异步⽅法的结构
上⾯是⼀个的使⽤async/await的例⼦(为了⽅便解说原理我才写的这样复杂的)。
使⽤async/await能⾮常简单的创建异步⽅法,防⽌耗时操作阻塞当前线程。
使⽤async/await来构建的异步⽅法,逻辑上主要有下⾯三个结构:
2.1 调⽤异步⽅法
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
AsyncMethod();//这个⽅法就是异步⽅法,异步⽅法的调⽤与⼀般⽅法完全⼀样
Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
}
注意:微软建议异步⽅法的命名是在⽅法名后添加Aysnc后缀,⽰例是我为了读起来⽅便做成了前缀,在真正构建异步⽅法的时候请注意⽤后缀。
异步⽅法的返回类型只能是void、Task、Task<TResult>。⽰例中异步⽅法的返回值类型是Task。
另外,上⾯的AsyncMethod()会被编译器提⽰报警,如下图:
因为是异步⽅法,所以编译器提⽰在前⾯使⽤await关键字,这个后⾯再说,为了不引⼊太多概念导致难以理解暂时就先这么放着。
writeline函数
2.2 异步⽅法本体
private async Task AsyncMethod()
{
var ResultFromTimeConsumingMethod = TimeConsumingMethod();
string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
Console.WriteLine(Result);
//返回值是`Task`的函数可以不⽤`return`,或者将`Task`改为void
}
⽤async来修饰⼀个⽅法,表明这个⽅法是异步的,声明的⽅法的返回类型必须为:void或Task或Task<TResult>。⽅法内部必须含有await修饰的⽅法,如果⽅法内部没有await关键字修饰的表达式,哪怕函数被async修饰也只能算作同步⽅法,执⾏的时候也是同步执⾏的。
被await修饰的只能是Task或者Task<TResule>类型,通常情况下是⼀个返回类型是Task/Task<TResult>的⽅法,当然也可以修饰⼀个Task/Task<TResult>变量,await只能出现在已经
⽤async关键字修饰的异步⽅法中。上⾯代码中就是修饰了⼀个变量ResultFromTimeConsumingMethod。
关于被修饰的对象,也就是返回值类型是Task和Task<TResult>函数或者Task/Task<TResult>类型的变量:如果是被修饰对象的前⾯⽤await修饰,那么返回值实际上是void或
者TResult(⽰例中ResultFromTimeConsumingMethod是TimeConsumingMethod()函数的返回值,也就是Task<string>类型,当ResultFromTimeConsumingMethod在前⾯加了await关键字后await ResultFromTimeConsumingMethod实际上完全等于ResultFromTimeConsumingMethod.Result)。如果没有await,返回值就是Task或者Task<TResult>。
2.3 耗时函数
//这个函数就是⼀个耗时函数,可能是`IO`密集型操作,也可能是`cpu`密集型⼯作。
private Task<string> TimeConsumingMethod()
{
var task = Task.Run(()=> {
Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
return "Hello I am TimeConsumingMethod";
});
return task;
}
这个函数才是真正⼲活的(为了让逻辑层级更分明,我把这部分专门做成了⼀个函数,在后⾯我会精简⼀下直接放到异步函数中,毕竟活在哪都是⼲)。
在⽰例中是⼀个CPU密集型的⼯作,我另开⼀线程让他拼命⼲了5s。如果是IO密集型⼯作⽐如⽂件读写等可以直接调⽤.Net提供的类库,对于这些类库底层具体怎么实现的?是⽤了多线程还是DMA?或者是多线程+DMA?这些问题我没有深究但是从表象看起来和我⽤Task另开⼀个线程去做耗时⼯作是⼀样的。
await只能修饰Task/Task<TResult>类型,所以这个耗时函数的返回类型只能是Task/Task<TResult>类型。
总结:有了上⾯三个结构就能完成使⽤⼀次异步函数。
3. async/await异步函数的原理
在开始讲解这两个关键字之前,为了⽅便,对某些⽅法做了⼀些拆解,拆解后的代码块⽤代号指定:
上图对⽰例代码做了⼀些指定具体就是:
Caller代表调⽤⽅函数,在上⾯的代码中就是button1_Click函数。
CalleeAsync代表被调⽤函数,因为代码中被调⽤函数是⼀个异步函数,按照微软建议的命名添加了Async后缀,在上⾯⽰例代码中就是AsyncMethod()函数。
CallerChild1代表调⽤⽅函数button1_Click在调⽤异步⽅法CalleeAsync之前的那部分代码。
CallerChild2代表调⽤⽅函数button1_Click在调⽤异步⽅法CalleeAsync之后的那部分代码。
CalleeChild1代表被调⽤⽅函数AsyncMethod遇到await关键字之前的那部分代码。
CalleeChild2代表被调⽤⽅函数AsyncMethod遇到await关键字之后的那部分代码。
TimeConsumingMethod是指被await修饰的那部分耗时代码(实际上我代码中也是⽤的这个名字来命名的函数)
⽰例代码的执⾏流程
这⾥涉及到了两个线程,线程ID分别是1和4。
Caller函数被调⽤,先执⾏CallerChild1代码,这⾥是同步执⾏与⼀般函数⼀样,然后遇到了异步函数CalleeAsync。
在CalleeAsync函数中有await关键字,await的作⽤是打分裂点。
编译器会把整个函数CalleeAsync从这⾥分裂成两个函数。await关键字之前的代码作为⼀个函数(按照我上⾯定义的指代,下⽂中就叫这部分代码CalleeChild1)await关键字之后的代码作为⼀个函数CalleeChild2。
CalleeChild1在调⽤⽅线程执⾏(在⽰例中就是主线程Thread1),执⾏到await关键字之后,另开⼀个线程耗时⼯作在Thread4中执⾏,然后⽴即返回。这时调⽤⽅会继续执⾏下⾯的代码CallerChild2(注意是Caller不是Callee)。
在CallerChild2被执⾏期间,TimeConsumingMethod也在异步执⾏(可能是在别的线程也可能是CPU不参与操作直接DMA的IO操作)。
当TimeConsumingMethod执⾏结束后,CalleeChild2也就具备了执⾏条件,⽽这个时候CallerChild2可能执⾏完了也可能没有,由于CallerChild2与CalleeChild2都会在Caller的线程执⾏,这⾥就会有冲
突应该先执⾏谁,编译器会在合适的时候在Caller的线程执⾏这部分代码。⽰意图如下:
请注意,CalleeChild2在上图中并没有画任何箭头,因为这部分代码的执⾏是由编译器决定的,暂时⽆法具体描述是什么时候执⾏。
总结:整个流程下来,除了TimeConsumingMethod函数是在Thread4中执⾏的,剩余代码都是在主线程Thread1中执⾏的。也就是说异步⽅法运⾏在当前同步上下⽂中,只有激活的时候才占⽤当前线程的时间,异步模型采⽤时间⽚轮转来实现。你也许会说,明明新加了⼀个Thread4线程怎么能说是运⾏在当前的线程中呢?这⾥说的异步⽅法运⾏在当前线程上的意思是由CalleeAsync分裂出来的CalleeChild1和CalleeChild2的确是运⾏在Thread1上的。
4. 带返回值的异步函数
之前的⽰例代码中异步函数是没有返回值的,作为理解原理⾜够了,但是在实际应⽤场景中,带返回值的应⽤才是最常⽤的。那么,上代码:
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
var ResultTask = AsyncMethod();
Console.WriteLine(ResultTask.Result);
Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
}
private async Task<string> AsyncMethod()
{
var ResultFromTimeConsumingMethod = TimeConsumingMethod();
string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
Console.WriteLine(Result);
return Result;
}
//这个函数就是⼀个耗时函数,可能是`IO`操作,也可能是`cpu`密集型⼯作。
private Task<string> TimeConsumingMethod()
{
var task = Task.Run(()=> {
Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
return "Hello I am TimeConsumingMethod";
});
return task;
}
主要更改的地⽅在这⾥:
按理说没错吧?然⽽,这代码⼀旦执⾏就会卡死。
4.1 死锁
是的,死锁。分析⼀下为什么:
按照之前我划定的代码块指定,在添加了新代码后CallerChild2与CalleeChild2的划分如上图。
这两部分代码块都是在同⼀个线程上执⾏的,也就是主线程Thread1,⽽且通常情况下CallerChild2是会早于CalleeChild2执⾏的(毕竟CalleeChild2得在耗时代码块执⾏之后执⾏)。Console.WriteLine(ResultTask.Result);(CallerChild2)其实是在请求CalleeChild2的执⾏结果,此时明显CalleeChild2还没有结束没有return任何结果,那Console.WriteLine(ResultTask.Result);就只能阻塞Thread1等待,直到CalleeChild2有结果。
然⽽问题就在这,CalleeChild2也是在Thread1上执⾏的,此时CallerChild2⼀直占⽤Thread1等待CalleeChild2的结果,耗时程序结束后轮到CalleeChild2执⾏的时候CalleeChild2⼜
因Thread1被CallerChild2占⽤⽽抢不到线程,永远⽆法return,那么CallerChild2就会永远等下去,这就造成了死锁。
4.2 解决⽅法
解决办法有两种⼀个是把Console.WriteLine(ResultTask.Result);放到⼀个新开线程中等待(个⼈觉得这⽅法有点⿇烦,毕竟要新开线程),还有⼀个⽅法是把Caller也做成异步⽅法:
ResultTask.Result变成了ResultTask 的原因上⾯也说了,await修饰的Task/Task<TResult>得到的是TResult。
之所以这样就能解决问题是因为嵌套了两个异步⽅法,现在的Caller也成了⼀个异步⽅法,当Caller执⾏到await后直接返回了(await拆分⽅法成两部分),CalleeChild2执⾏之后才轮到Caller中await后⾯的代码块(Console.WriteLine(ResultTask.Result);)。
另外,把Caller做成异步的⽅法也解决了⼀开始的那个警告,还记得么?
5. 质疑
到现在,你可能会说:使⽤async/await不⽐直接⽤Task.Run()来的简单啊?⽐如我⽤Task的TaskContinueWith⽅法也能实现:
private void button1_Click(object sender, EventArgs e)
{
var ResultTask = Task.Run(()=> {
Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
return "Hello I am TimeConsumingMethod";
});
ResultTask.ContinueWith(OnDoSomthingIsComplete);
}
private void OnDoSomthingIsComplete(Task<string> t)
{
Action action = () => { textBox1.Text = t.Result;};
textBox1.Invoke(action);
Console.WriteLine("Continue Thread ID :" + Thread.CurrentThread.ManagedThreadId);
}
是的,上⾯的代码也能实现。但是,async/await的优雅的打开⽅式是这样的:
private async void button1_Click(object sender, EventArgs e)
{
var t = Task.Run(() => {
Thread.Sleep(5000);
return "Hello I am TimeConsumingMethod";
});
textBox1.Text = await t;
}
看到没,惊不惊喜,意不意外,寥寥⼏⾏就搞定了,不⽤再多写那么多函数,使⽤起来也很灵活。最让⼈头疼的跨线程修改控件的问题完美解决了,再也不⽤使⽤Invoke了,因为修改控件的操作压根就是在原来的线程上做的,还能不阻塞UI。
注:感谢17楼和23楼同学对死锁的解释,并给出了其它解决办法,你们是对的!同时也感谢其他同学的互动参与,通过交流相互提⾼才是我们想要的!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论