上一篇末尾提到了Awaiter这个类型,上一篇说了,能await的对象,必须包含GetAwaiter()方法,不清楚的朋友可以看上篇文章。那么,Awaiter到底有什么特别之处呢?

  首先,从上篇文章我们知道,一个Awaiter必须实现INotifyCompletion接口,这个接口定义如下:

namespace System.Runtime.CompilerServices
{
/// <summary>
/// Represents an operation that will schedule continuations when the operation completes.
/// </summary>
public interface INotifyCompletion
{
/// <summary>Schedules the continuation action to be invoked when the instance completes.</summary>
/// <param name="continuation">The action to invoke when the operation completes.</param>
/// <exception cref="System.ArgumentNullException">The <paramref name="continuation"/> argument is null (Nothing in Visual Basic).</exception>
void OnCompleted(Action continuation);
}
}

  除此之外还必须包含IsCompleted属性和包含GetResult()方法。

  注意OnCompleted的参数是一个Action委托,并且不出意外的话,委托里面总会有一个地方调用一个MoveNext()方法,它推动状态机到达下一个状态,然后执行下一个状态需要执行的代码。

  那么,知道这个有什么用呢?第一,它是你充分了解async/await这套机制的基础,包括与之相关的同步上下文、执行上下文、死锁问题等,第二,它可以实现一些特殊的功能。

  从上一篇我们知道,OnCompleted中的contination的主要目的是推动状态机的执行,也就是推动异步方法中await后面部分的代码执行。从这里看出,continuation的执行是受我们控制的,因此我们可以直接执行它,或是等待某个条件成熟然后执行它,我们可以把它放到线程池执行,也可以单独起一个线程执行。譬如,我们可以让await后面部分的代码直接在线程池上执行。

public static async Task AwaiterTest()
{
Console.WriteLine($"是否是线程池线程?{Thread.CurrentThread.IsThreadPoolThread}");
await default (SkipToThreadPoolAwaiter);
Console.WriteLine($"是否是线程池线程?{Thread.CurrentThread.IsThreadPoolThread}");
} static void Main(string[] args)
{
_ = AwaiterTest();
Console.ReadLine();
} public struct SkipToThreadPoolAwaiter : INotifyCompletion
{
public bool IsCompleted => false;
public void GetResult()
{
Console.WriteLine("调用GetResult以获取结果");
}
public void OnCompleted(Action continuation)
{
Console.WriteLine("调用OnCompleted,把Await后面部分要执行的代码传递过来(传递MoveNext,以推动状态机流转)");
ThreadPool.QueueUserWorkItem(state =>
{
Console.WriteLine("开始执行Await后面部分的代码");
continuation();
Console.WriteLine("后面部分的代码执行完毕");
});
Console.WriteLine("返回调用线程");
} public SkipToThreadPoolAwaiter GetAwaiter()
{
Console.WriteLine("获得Awaiter");
return this;
}
}

  这是一个控制台程序,输出结果如下。

是否是线程池线程?False
获得Awaiter
调用OnCompleted,把Await后面部分要执行的代码传递过来(传递MoveNext,以推动状态机流转)
返回调用线程
开始执行Await后面部分的代码
调用GetResult以获取结果
是否是线程池线程?True
后面部分的代码执行完毕

  特别注意一下,第五步说明可能有点疑惑,怎么第六步不是打印是否是线程池线程?原因是部分awaiter是有返回值的,在执行await后面部分的代码时,会首先调用GetResult()以获取结果。这对编译器改造异步方法来说是一个固定的模式(上篇文章没有体现这一步)。

  把Awaiter改成有返回值尝试。

public static async Task AwaiterTest()
{
Console.WriteLine($"是否是线程池线程?{Thread.CurrentThread.IsThreadPoolThread}");
var res = await default (SkipToThreadPoolAwaiter);
Console.WriteLine($"结果是{res}");
Console.WriteLine($"是否是线程池线程?{Thread.CurrentThread.IsThreadPoolThread}");
} static void Main(string[] args)
{
_ = AwaiterTest();
Console.ReadLine();
} public struct SkipToThreadPoolAwaiter : INotifyCompletion
{
public bool IsCompleted => false;
public int GetResult()
{
Console.WriteLine("调用GetResult以获取结果");
return 1;
}
public void OnCompleted(Action continuation)
{
Console.WriteLine("调用OnCompleted,把Await后面部分要执行的代码传递过来(传递MoveNext,以推动状态机流转)");
ThreadPool.QueueUserWorkItem(state =>
{
Console.WriteLine("开始执行Await后面部分的代码");
continuation();
Console.WriteLine("后面部分的代码执行完毕");
});
Console.WriteLine("返回调用线程");
} public SkipToThreadPoolAwaiter GetAwaiter()
{
Console.WriteLine("获得Awaiter");
return this;
}
}

  输出如下

是否是线程池线程?False
获得Awaiter
调用OnCompleted,把Await后面部分要执行的代码传递过来(传递MoveNext,以推动状态机流转)
返回调用线程
开始执行Await后面部分的代码
调用GetResult以获取结果
结果是1
是否是线程池线程?True
后面部分的代码执行完毕

  对照前面的文章来看,相信你应该有所得,能解决你部分的疑惑。前面说到,我们可以控制continuation的执行,那如果当前线程有同步上下文(SychronizationContext),我们是不是可以放到同步上下文中执行?TaskAwaiter是会这么做的,如果你不想它使用同步上下文,你可以在Task实例上调用ConfigureAwait(false),它表面后面部分的代码将不会使用同步上下文执行。

  另外说一下Task.Yield()这个Awaiter,他的行为是捕捉同步上下文,如果有,则会放到同步上下文中执行,如果没有,则会放到线程池中执行。在窗体程序中,有时候你打开一个模态对话框,会导致主窗体部分的动画没有反应,在模态对话框关闭之后,才会反应。原因是模态对话框阻塞了主窗体的消息循环,也就是阻塞了主线程,如果想让动画先完成,然后再打开模态对话框,则可以在打开模态对话框之前,Await Task.Yield(),这也对应了它的意思,让渡之意。

  后面文章还会说明同步上下文具体是什么、异步代码中使用同步代码会导致死锁的本质原因、如何实现类似Task的类,并且怎么与Async/await这套机制搭配使用等知识。

  觉得有收获的不妨点个赞,有支持才有动力写出更好的文章。(.Net深入学习交流群(617374043),欢迎加入!)

C#异步编程由浅入深(三)细说Awaiter的更多相关文章

  1. 【C# TAP 异步编程】三、async\await的运作机理详解

    [原创] 本文只是个人笔记,很多错误,欢迎指出. 环境:vs2022  .net6.0 C#10 参考:https://blog.csdn.net/brook_shi/article/details/ ...

  2. C#异步编程由浅入深(一)

    一.什么算异步?   广义来讲,两个工作流能同时进行就算异步,例如,CPU与外设之间的工作流就是异步的.在面向服务的系统中,各个子系统之间通信一般都是异步的,例如,订单系统与支付系统之间的通信是异步的 ...

  3. C#异步编程由浅入深(二)Async/Await的作用.

      考虑到直接讲实现一个类Task库思维有点跳跃,所以本节主要讲解Async/Await的本质作用(解决了什么问题),以及Async/Await的工作原理.实现一个类Task的库则放在后面讲.首先回顾 ...

  4. c#异步编程(三)—ASP.NET MVC 异步控制器及EF异步操作

    ASP.NET MVC 异步控制器及EF异步操作 异步控制器 ASP.NET MVC2后开始了对异步请求管道的支持,异步请求管道的作用是允许web服务器处理长时间运行的请求,比如 那些花费大量时间等待 ...

  5. Dart 异步编程(三):详细认识

    基本概念 普通任务按照顺序执行:异步任务将在未来的某个时间执行. 实际演示 void main() { // waitFuture 函数是一个异步函数,阻塞会发生在函数内部 waitFuture(); ...

  6. C#异步编程(三)内核模式线程同步

    其实,在开发过程中,无论是用户模式的同步构造还是内核模式,都应该尽量避免.因为线程同步都会造成阻塞,这就影响了我们的并发量,也影响整个应用的效率.不过有些情况,我们不得不进行线程同步. 内核模式 wi ...

  7. C#中的异步编程Async 和 Await

    谈到C#中的异步编程,离不开Async和Await关键字 谈到异步编程,首先我们就要明白到底什么是异步编程. 平时我们的编程一般都是同步编程,所谓同步编程的意思,和我们平时说的同时做几件事情完全不同. ...

  8. Java 异步编程的几种方式

    前言 异步编程是让程序并发运行的一种手段.它允许多个事情同时发生,当程序调用需要长时间运行的方法时,它不会阻塞当前的执行流程,程序可以继续运行,当方法执行完成时通知给主线程根据需要获取其执行结果或者失 ...

  9. C#与C++的发展历程第三 - C#5.0异步编程巅峰

    系列文章目录 1. C#与C++的发展历程第一 - 由C#3.0起 2. C#与C++的发展历程第二 - C#4.0再接再厉 3. C#与C++的发展历程第三 - C#5.0异步编程的巅峰 C#5.0 ...

随机推荐

  1. 论文解读二代GCN《Convolutional Neural Networks on Graphs with Fast Localized Spectral Filtering》

    Paper Information Title:Convolutional Neural Networks on Graphs with Fast Localized Spectral Filteri ...

  2. Java加减乘除计算器实现

    加减乘除计算机的实现 public static void main(String[] args) { while(true) { System.out.println("依次输入要计算的2 ...

  3. 将Cesium Tools用于更好的构建管理

    Cesium中文网:http://cesiumcn.org/ | 国内快速访问:http://cesium.coinidea.com/ Cesium技术正在给建筑业带来革命性的变化.我们与 partn ...

  4. golang中for循环的常用用法

    package main import "fmt" func main() { //printFormula() // 打印九九乘法表 //printLeftTriangle() ...

  5. Redis内存满了怎么办(新年快乐)

    Redis内存满了怎么办(新年快乐) 入我相思门,知我相思苦. 长相思兮长相忆,短相思兮无穷极. 一.配置文件 Redis长期使用或者不设置过期时间,导致内存爆满或不足,可以到Redis的配置文件re ...

  6. python项目中 ,open() 方法, 如何读取json文件的位置。

    一 copy 目标文件绝对路径的URL. 固定在你电脑上的路径.简单好用.

  7. 如何在 VS Code 中为 Java 类生成序列化版本号

    前言 IDEA 提供自动生成序列化版本号的功能,其实 VS Code 也可以,只是默认关闭了这个功能,下面就来看看如何开启这个功能吧. 配置过程 首先需要保证 VS Code 上安装了提供 Java ...

  8. ApacheCN Golang 译文集 20211025 更新

    Go 云原生编程 零.前言 一.现代微服务架构 二.使用 RESTAPI 构建微服务 三.保护微服务 四.使用消息队列的异步微服务架构 五.使用 React 构建前端 六.在容器中部署应用 七.AWS ...

  9. AT3913 XOR Tree

    经过长时间的思考,我发现直接考虑对一条链进行修改是很难做出本题的,可能需要换一个方向. 可以发现本题中有操作的存在,是没有可以反过来做的做法的,因此正难则反这条路应该走不通. 那么唯一的办法就是简化这 ...

  10. Swift 类的构造函数

    构造函数的介绍 构造函数类似于OC中的初始化方法:init方法 默认情况下载创建一个类时,必然会调用一个构造函数 即便是没有编写任何构造函数,编译器也会提供一个默认的构造函数. 如果是继承自NSObj ...