C# 完全搞懂async/await

前言

Talk is cheap, Show you the code first!html

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
}

//这个函数就是一个耗时函数,多是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真的炒鸡优雅。异步

异步方法的结构

上面是一个的使用async/await的例子(为了方便解说原理我才写的这样复杂的)。
使用async/await能很是简单的建立异步方法,防止耗时操做阻塞当前线程。
使用async/await来构建的异步方法,逻辑上主要有下面三个结构:async

调用异步方法

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后缀,示例是我为了读起来方便作成了前缀,在真正构建异步方法的时候请注意用后缀。(好吧我认可是我忘记了,而后图片也都截好了再修改太麻烦了。。。。就懒得从新再修改了)ide

异步方法的返回类型只能是voidTaskTask<TResult>。示例中异步方法的返回值类型是Task函数

另外,上面的AsyncMethod()会被编译器提示报警,以下图:

由于是异步方法,因此编译器提示在前面使用await关键字,这个后面再说,为了避免引入太多概念致使难以理解暂时就先这么放着。ui

异步方法本体

private async Task AsyncMethod()
{
    var ResultFromTimeConsumingMethod = TimeConsumingMethod();
    string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
    Console.WriteLine(Result);
    //返回值是Task的函数能够不用return
}

async来修饰一个方法,代表这个方法是异步的,声明的方法的返回类型必须为:voidTaskTask<TResult>。方法内部必须含有await修饰的方法,若是方法内部没有await关键字修饰的表达式,哪怕函数被async修饰也只能算做同步方法,执行的时候也是同步执行的。spa

被await修饰的只能是Task或者Task<TResule>类型,一般状况下是一个返回类型是Task/Task<TResult>的方法,固然也能够修饰一个Task/Task<TResult>变量,await只能出如今已经用async关键字修饰的异步方法中。上面代码中就是修饰了一个变量ResultFromTimeConsumingMethod线程

关于被修饰的对象,也就是返回值类型是TaskTask<TResult>函数或者Task/Task<TResult>类型的变量:若是是被修饰对象的前面用await修饰,那么返回值其实是void或者TResult(示例中ResultFromTimeConsumingMethodTimeConsumingMethod()函数的返回值,也就是Task<string>类型,当ResultFromTimeConsumingMethod在前面加了await关键字后 await ResultFromTimeConsumingMethod实际上彻底等于 ResultFromTimeConsumingMethod.Result)。若是没有await,返回值就是Task或者Task<TResult>翻译

耗时函数

//这个函数就是一个耗时函数,多是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>类型。

总结:有了上面三个结构就能完成使用一次异步函数。

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和3。

Caller函数被调用,先执行CallerChild1代码,这里是同步执行与通常函数同样,而后遇到了异步函数CalleeAsync。

在CalleeAsync函数中有await关键字,await的做用是打分裂点。

编译器会把整个函数(CalleeAsync)从这里分裂成两个函数。await关键字以前的代码做为一个函数(按照我上面定义的指代,下文中就叫这部分代码CalleeChild1)await关键字以后的代码做为一个函数(CalleeChild2)。

CalleeChild1在调用方线程执行(在示例中就是主线程Thread1),执行到await关键字以后,另开一个线程耗时工做在Thread3中执行,而后当即返回。这时调用方会继续执行下面的代码CallerChild2(注意是Caller不是Callee)。

在CallerChild2被执行期间,TimeConsumingMethod也在异步执行(多是在别的线程也多是CPU不参与操做直接DMA的IO操做)。

当TimeConsumingMethod执行结束后,CalleeChild2也就具有了执行条件,而这个时候CallerChild2可能执行完了也可能没有,因为CallerChild2与CalleeChild2都会在Caller的线程执行,这里就会有冲突应该先执行谁,编译器会在合适的时候在Caller的线程执行这部分代码。示意图以下:

请注意,CalleeChild2在上图中并无画任何箭头,由于这部分代码的执行是由编译器决定的,暂时没法具体描述是何时执行。

总结一下:

整个流程下来,除了TimeConsumingMethod函数是在Thread3中执行的,剩余代码都是在主线程Thread1中执行的.

也就是说异步方法运行在当前同步上下文中,只有激活的时候才占用当前线程的时间,异步模型采用时间片轮转来实现(这一点我没考证,仅做参考)。

你也许会说,明明新加了一个Thread3线程怎么能说是运行在当前的线程中呢?这里说的异步方法运行在当前线程上的意思是由CalleeAsync分裂出来的CalleeChild1和CalleeChild2的确是运行在Thread1上的。

带返回值的异步函数

以前的示例代码中异步函数是没有返回值的,做为理解原理足够了,可是在实际应用场景中,带返回值的应用才是最经常使用的。那么,上代码:

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;
}

主要更改的地方在这里:


按理说没错吧?然而,这代码一旦执行就会卡死。

死锁

是的,死锁。分析一下为何:

按照以前我划定的代码块指定,在添加了新代码后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就会永远等下去,这就形成了死锁。

解决办法有两种一个是把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作成异步的方法也解决了一开始的那个警告,还记得么?

这样没省多少事啊?

到如今,你可能会说:使用async/await不比直接用Task.Run()来的简单啊?好比我用TaskTaskContinueWith方法也能实现:

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。

参考:
死锁问题 http://www.javashuo.com/article/p-gqbuqdfo-bx.html
该博主是翻译的英文资料,英文原文:http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
https://www.cnblogs.com/zhili/archive/2013/05/15/Csharp5asyncandawait.html
http://www.cnblogs.com/heyuquan/archive/2013/04/26/3045827.html
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/index

相关文章
相关标签/搜索