关于C# async/await的一些说明

关于C# async/await的一些说明


下文以个人对async/await的理解为基础进行一些说明。


1、自定义的几个关键概念

  1. 调用流阻塞:不同于线程阻塞,调用流阻塞只对函数过程起作用,调用流阻塞表示在一次函数调用中,执行函数代码的过程中发生的无法继续往后执行,需要在函数体中的某个语句停止的情形;
  1. 调用流阻塞点:调用流阻塞中,执行流所停下来地方的那条语句;
  2. 调用流阻塞返回:不同于线程阻塞,调用流发生阻塞的时候,调用流会立即返回,在C#中,返回的对象可以是Task或者Task<T>
  3. 调用流阻塞异步完成跳转:当调用流阻塞点处的异步操作完成后,调用流被强制跳转回调用流阻塞点处执行下一个语句的情形;
  4. async传染:指的是根据C#的规定:若某个函数F的函数体中需要使用await关键字的函数必须以async标记,进一步导致需要使用await调用F的那个函数F'也必须以async标记的情况;
  5. Task对象的装箱与拆箱:指Task<T>和T能够相互转换的情况。
  6. 异步调用:指以await作为修饰前缀进行方法调用的调用形式,异步调用时会发生调用流阻塞。
  7. 同步调用:指不以await作为修饰前缀进行方法调用的调用形式,同步调用时不会发生调用流阻塞。

2、async/await的使用场景

async/await用于异步操作。

在使用C#编写GUI程序的时候,如果有比较耗时的操作(如图片处理、数据压缩等),我们一般新开一个线程把这些工作交给这个线程处理,而不放到主线程中进行操作,以免阻塞UI刷新,造成程序假死。

传统的做法是直接使用C#的Thread类(也存在别的方式,参考这篇文章)进行操作。传统的做法在复杂的应用编写中可能会出现回调地狱的问题,因此C#目前主要推荐使用async/await来进行异步操作。

async/await通过对方法进行修饰把C#中的方法分为同步方法和异步方法两类,异步方法命名约定以Async结尾。但是需要注意的是,在调用异步方法的时候,并非一定是以异步方式来进行调用,只有指定了以await为修饰前缀的方法调用才是异步调用

3、async/await的调用过程

考虑以下C#程序:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            TestMain();
        }

        static void TestMain()
        {
            Console.Out.Write("Start\n");
            GetValueAsync();
            Console.Out.Write("End\n");
            Console.ReadKey();
        }
        
        static async Task GetValueAsync()
        {
            await Task.Run(()=>
            {
                Thread.Sleep(1000);
                for(int i = 0; i < 5; ++i)
                {
                    Console.Out.WriteLine(String.Format("From task : {0}", i));
                }
            });
            Console.Out.WriteLine("Task End");
        }
    }
}

在我的计算机上,执行该程序得到以下结果:

Start
End
From task : 0

From task : 1
From task : 2
From task : 3
From task : 4
Task End

下面来分析该程序的执行流程:

  1. Main()调用TestMain(),执行流转入TestMain();
  1. 打印Start
  2. 调用GetValueAsync(),执行流转入GetValueAsync(),注意此处是同步调用;
  3. 执行Task.Run(),生成一个新的线程并执行,同时立即返回一个Task对象;
  4. 由于调用Task.Run()时,是以await作为修饰的,因此是一个异步调用,上下文环境保存第4步中返回的Task对象,在此处发生调用流阻塞,而当前的调用语句便是调用流阻塞点,于是发生调用流阻塞返回,执行流回到AysncCall()的GetValueAsync()处,并执行下一步

第5步之后就不好分析了,因为此时已经新建了一个线程用来执行后台线程,如果计算机速度够快,那么由于新建的线程代码中有一个Thread.Sleep(1000);,因此线程会被阻塞,于是主线程会赶在新建的线程恢复执行之前打印End然后Console.ReadKey()在这里我假设发生的是这个情况,然后进入下面的步骤

  1. 新的线程恢复执行,打印0 1 2 3 4 5,线程执行结束,Task对象的IsCompleted变成true
  1. 此时执行流(强制被)跳转到调用流阻塞点,即从调用流阻塞点恢复执行流,发生了调用流阻塞异步完成跳转,于是打印Task End
  2. 程序执行流结束;

仔细研究以上流程,可以发现async/await最重要的地方就是调用流阻塞点,这里的阻塞并不是阻塞的线程,而是阻塞的程序执行流。整个过程就像是一个食客走进一间饭馆点完菜,但是厨师说要等半个小时才做好(调用流阻塞),于是先给这个食客开了张单子(调用流阻塞点)让他先去外面逛一圈(调用流阻塞返回),等时间到了会通知他然后他再拿这张票来吃饭(调用流阻塞异步完成跳转);整个过程中这个食客并没有在饭馆做下来等(线程阻塞),而是又去干了别的事情了。在这里,await就是用来指定调用流阻塞点的关键字,而async则是用来标识某个方法可以被调用流阻塞的关键字。

4、假如不用await?

如果我们不使用await异步调用方法F的话,那么方法F将会被当成同步方法调用,即发生同步调用,这个时候执行流不会遇到调用流阻塞点,因此会直接往下执行,考虑上面的代码如果写成:

        static async Task GetValueAsync()
        {
            Task.Run(()=>
            {
                Thread.Sleep(1000);
                for(int i = 0; i < 5; ++i)
                {
                    Console.Out.WriteLine(String.Format("From task : {0}", i));
                }
            });
            Console.Out.WriteLine("Task End");
        }

那么执行流不会在Task.Run()这里停下返回,而是直接“路过”这里,执行后面的语句,打印出Task End,然后和一般的程序一样返回。当然新的线程还是会被创建出来并执行,但是这种情况下的程序就不会去等Task.Run()完成了。在我的计算机上输出的结果如下:

Start
Task End
End
From task : 0
From task : 1
From task : 2
From task : 3
From task : 4

5、async传染与病源隔断方法

根据C#的规定:若某个函数F的函数体中需要使用await关键字则该函数必须以async标记,此时F成为异步方法,于是,这会导致这样子的情况:需要使用await调用F的那个函数F'也必须以async标记

这个现象我称之为async传染

同时,C#又规定,Main函数不能够是异步方法,这意味着至少在Main函数中是不能够出现await异步调用的,进一步说明了任何的异步调用都是同步调用的子调用,而调用异步方法的那个方法我称之为病源隔断方法,因为在这里开始,不再会发生async传染。

而在病源隔断方法中,一般会在其他操作完成之后去等待异步操作完成:

// 病源隔断方法
void M()
{
    var task = F();
    DoSomething();
    if(task.IsCompleted)
    {
        // 类似Thread的join()方法
        task.Wait();
    }
}

// 异步方法
async Task F() 
{
    await DoAsync();
}

5、如果异步方法要返回值?

在上面的例子中,异步方法都是返回的Task,表示没有返回值。而如果要返回值的话,那么就简单地把Task换成Task<T>就行了,其中T是你的返回值的类型。

C#的Task<T>会自动和T完成装箱拆箱操作。也就是说如果异步方法F返回Task<int>对象,那么当异步方法完成的时候,它会自动变成int,整个过程由编译器完成:

void async M()
{
    int r = await F();
}

// 异步方法
async Task<int> F() 
{
    await DoAsync();
    return 0;
}

这里说C#会自动完成Task<int>到T的装箱和拆箱事实上是不严谨的,因为编译器为我们隐藏了很多细节,这里只是“看起来”像是有这么个过程,但实质上并非如此。

事实上异步方法的返回值声明声明的只是调用阻塞返回值,并不是异步方法执行完成后的真正返回值。造成这个事实的主要原因是存在调用阻塞返回真实方法返回两个返回值,前一个是“临时”的,而后一个是“执行完成后”的,因此我们可以认为Task<int>对应的是调用阻塞返回的返回值,而T这对应的是真实方法返回的返回值。

我们可以把M进行改写,事实上编译器是为我们做了类似下面这样子的工作:

void M()
{
    int r;
    Task<int> t = 获取调用F()时的调用阻塞点的Task<int>对象;
    t.OnCompleted += () => {
        r = (int)t.Value;
    };
    t.Wait();
}

6、异步方法的定义约束

首先要明白的一点,就是async/await是不会主动创建线程(Task)的,创建线程的工作还是交给程序员来完成;async/await说白了就只是用来提供阻塞调用点的关键字而已。

因此,如果我们要定义一个异步方法,那么至少要保证:

  1. 在异步方法的调用中会出现新的线程(Task),无论调用层数有多深
  2. 一个新线程(Task)应该有且仅有一个阻塞调用点
  3. 异步方法嵌套调用的时候, 每个嵌套调用的异步方法内部至少要调用一个异步方法或者await一个返回值为Task的同步方法。

7、一个容易误解的地方

考虑以下代码:

async int M()
{
    return await F();
}

其中F()是一个异步方法,它返回的是Task<int>对象。

这段代码事实上等价于:

async int M()
{
    int r = await F();
    return r;
}

注意和

async Task<int> M()
{
    return F();
}

区分。后面这段代码是一个同步方法,它只会返回F()的真实返回值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容