天天看点

多线程之旅(4)_async/await的用法精细详解

GitHub源码地址:稍后

async/await是个常见但不常用的方法。常见是因为在比较官方的代码。片段里经常见到这样的搭配,不常用是因为作为开发人员来说,我们常常有更熟知的方法去代替他。

async/await到底有什么用呢,其实网上也很少有说的明白的文章,下面我来尽量简单明了的解释一下。

1.使用场景

async/await这两个关键字用在线程同步/异步的场景中。

2.语法方法

async和await是一种搭配用法,可以理解为两者一般会同时出现。在写一个方法时,async写在定义方法的地方,await则写在定义的方法内部。如下所示:

async Task<int> f() {
    var value = await Task.Run(()=>gosleep())
    return value.Reslut;
}

public static int gosleep()
{
    Thread.Sleep(1000);
        return 1;
}
           

当然你也可以只写async而不写await,程序会给你一些警告,建议你不要这样做,因为这种做法没有任何意义,这种做法会导致你不得不去阻塞主进程。具体原因后面会讲

3.功能作用

async/await这两个关键词第一个很重要的用处,是说明作用,或者说可以增加代码的可读性。第二个作用就是表明哪些代码要进行同步处理

当定义函数出现async时,说明这个函数中有异步的功能。这个函数要异步执行。

当函数内部出现await关键字时,说明在await后面跟的方法就是一个异步的方法。

4.使用async/await的特性和优点

await这个关键字后面表示启动了一个线程,那问题来了,我们用Thread不行吗,用普通的Task不行吗?

首先,《C#并发编程经典实例》这本书上说过,编写多线程的时候,当你开始写new Thread时,你就已经输了。Task是现在更好的线程处理方式,而Thread已经开始过时了。

其次,Thread一旦释放出去,基本就无法掌握其运行状态。比如:这个Thread线程所处理的工作结束了没有?它应该生成的数据生成完没有?我主进程是不是已经可以使用这个线程生成的数据了?如果线程还没有结束,主进程还要等待多久?

当然我们可以通过一些投机取巧的方法解决这个问题,比如设置一些标记,当线程运行完成时,修改这个标记的值,主进程如果想要使用线程所生成的数据,就去循环判断这个标记值是否被标记为完成(虽然看起来比较傻,但事实上这有可能是最安全的方法)。

如果你不想用这种很傻的,很容易被同事和领导吐槽的方法,你就可以考虑在调用的方法中使用await这个关键字。

(1)使用包含await标记的线程,一般是可以拥有返回值的,也就是线程可以用return来返回自己所生成的数据。

(2)当主进程需要用到这个线程返回的数据时,如果线程已经执行完成,主进程可以直接获取线程生成的数据;

(3)如果这个线程还没有执行完,当主进程试图获取线程的返回值的时候,就会开始等待,不再继续执行。也就是主进程会阻塞等待,从而避免拿到错误的数据。

(4)直到线程处理完成后,主进程成功读取到线程return的数据,然后继续执行后续的代码。

最后,总的来说,如果你的程序:

(1)又想启用多线程模式,

(2)主进程里后续又有代码必须依赖前面异步线程的计算结果,

(3)又不能很好的控制这两者之间的时长间隔,

(4)又想尽量减少主进程的阻塞,

那么就来用async/await吧。而且一旦你开始使用await后,你就会发现Task原来有一堆方便的线程同步方法可以一起使用,比如WaitAll,WhenAll,ContinueWith,Delay,GetAwaiter等等一套线程同步全家桶,用起来非常方便。

5.async/await的使用注意事项

最后在描述一遍async/await的用法,当一个被调用的方法中有线程时,这个方法要用关键字async修饰,这个方法中关于线程调用的代码前,要用await关键字修饰。代码见第二部分给出的示例。

当主进程走到代码内部时,如果发现有await关键字,就知道此处有线程要异步执行,主进程就会自动执行后续的其他代码,直到遇到需要使用此线程返回值得地方,主进程才会考虑是否要停下来等待。这样就解决了主进程阻塞的问题。

需要注意的地方是什么呢?

async/await一定要一起使用,否则会失去意义。只使用async一个关键字时,再返回结果的时候就会报错,不能返回一个Task<int>类型结果如下图:

多线程之旅(4)_async/await的用法精细详解

程序会说报错:

多线程之旅(4)_async/await的用法精细详解

因此你只能强行改变返回值类型,最后修改成如下形式:

多线程之旅(4)_async/await的用法精细详解

ok,你在返回数据的时候,改成了return result.Result这种返回值,此时程序不再报错。但事实上此时的程序已经不再是异步的了。

原因很简单:return result.Result中的Result,其实是var result = Task.Run(() => gosleep20())这行代码里返回的数据结果;如果要获取result.Result,也就说明程序要在return result.Result这行代码上阻塞主进程,换句话说是主进程会被困在这个假异步方法中出不去;直到var result = Task.Run(() => gosleep20())这行代码执行完成,才能获取到result.Result的数据,同时放开阻塞,主进程才能继续运行。

因此这个异步也就变得没有意义了。

6.async/await的数据流转图

下面这个图

多线程之旅(4)_async/await的用法精细详解

关系图中的数值对应于以下步骤。

1.事件处理程序调用并等待 AccessTheWebAsync 异步方法。

2.AccessTheWebAsync 可创建 HttpClient 实例并调用 GetStringAsync 异步方法以下载网站内容作为字符串。

3.GetStringAsync 中发生了某种情况,该情况挂起了它的进程。 可能必须等待网站下载或一些其他阻止活动。 为避免阻止资源,GetStringAsync 会将控制权出让给其调用方 AccessTheWebAsync。

GetStringAsync 返回 Task,其中 TResult 为字符串,并且 AccessTheWebAsync 将任务分配给 getStringTask 变量。 该任务表示调用 GetStringAsync 的正在进行的进程,其中承诺当工作完成时产生实际字符串值。

4.由于尚未等待 getStringTask,因此,AccessTheWebAsync 可以继续执行不依赖于 GetStringAsync 得出的最终结果的其他工作。 该任务由对同步方法 DoIndependentWork 的调用表示。

5.DoIndependentWork 是完成其工作并返回其调用方的同步方法。

6.AccessTheWebAsync 已用完工作,可以不受 getStringTask 的结果影响。 接下来,AccessTheWebAsync 需要计算并返回该下载字符串的长度,但该方法仅在具有字符串时才能计算该值。

因此,AccessTheWebAsync 使用一个 await 运算符来挂起其进度,并把控制权交给调用 AccessTheWebAsync 的方法。 AccessTheWebAsync 将 Task(Of Integer) 或 Task<int> 返回至调用方。 该任务表示对产生下载字符串长度的整数结果的一个承诺。

 备注

如果 GetStringAsync(因此 getStringTask)在 AccessTheWebAsync 等待前完成,则控件会保留在 AccessTheWebAsync 中。如果异步调用过程 (getStringTask) 已完成,并且 AccessTheWebSync 不必等待最终结果,则挂起然后返回到 AccessTheWebAsync 将造成成本浪费。

在调用方内部(此示例中的事件处理程序),处理模式将继续。 在等待结果前,调用方可以开展不依赖于 AccessTheWebAsync 结果的其他工作,否则就需等待片刻。事件处理程序等待 AccessTheWebAsync,而 AccessTheWebAsync 等待 GetStringAsync。

7.GetStringAsync 完成并生成一个字符串结果。 字符串结果不是通过按你预期的方式调用 GetStringAsync 所返回的。(请记住,此方法已在步骤 3 中返回一个任务。)相反,字符串结果存储在表示完成方法 getStringTask 的任务中。 await 运算符从 getStringTask 中检索结果。 赋值语句将检索到的结果赋给 urlContents。

8.当 AccessTheWebAsync 具有字符串结果时,该方法可以计算字符串长度。 然后,AccessTheWebAsync 工作也将完成,并且等待事件处理程序可继续使用。 在此主题结尾处的完整示例中,可确认事件处理程序检索并打印长度结果的值。

如果你不熟悉异步编程,请花 1 分钟时间考虑同步行为和异步行为之间的差异。 当其工作完成时(第 5 步)会返回一个同步方法,但当其工作挂起时(第 3 步和第 6 步),异步方法会返回一个任务值。 在异步方法最终完成其工作时,任务会标记为已完成,而结果(如果有)将存储在任务中。

7.总结

好了,搞了这么老半天终于说完了

附上两个很厉害的链接:

https://docs.microsoft.com/zh-cn/previous-versions/hh191443(v=vs.120)

https://docs.microsoft.com/zh-cn/previous-versions/hh873191%28v%3dvs.120%29

测试代码见文首GitHub链接

继续阅读