1.同步与异步

假设存在

IO事件A:请求网络资源 (完成耗时5s)

IO事件B:查询数据库 (完成耗时5s)

情况一:线程1工人在发起A请求后,一直阻塞等待,在A响应返回结果后再接着处理事件B,那总共需要耗时>10s.

情况二:线程1工人在发起A请求后,马上返回发起B请求然后返回,5s后事件A响应返回,接着事件B响应返回,那总共需要耗时<10s.

情况一就是同步的概念,而情况二就是异步的概念。细节会有所不同,但大致上可以这样理解。然而并不是所有情况适用异步,下面将会解释。

2.异步运行的顺序

   c#中的异步关键词是async与await,常常结合Task使用,如下面实例,看看它执行的情况

 static async Task Main(string[] args)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:MainStart"); //标记
await SayHi();
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:MainEnd"); //标记4
} static async Task SayHi()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:SayHiStart"); //标记
await Task.Delay();
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:SayHiEnd"); //标记3
}

结果:

1:MainStart
1:SayHiStart
5:SayHiEnd
5:MainEnd

c#7.1后的版本都支持异步main方法,程序执行的状况

线程1->标记1,

线程1->标记2,

线程5->标记3

线程5->标记4

执行顺序如预期,而需要关注的是线程在执行期间的切换,在线程1执行完标记2后就已经返回,接着由线程5接管了后面代码逻辑的执行,那到底为什么会发生这样的情况?

答案是:编译器会自动地替我们完成了大量了不起的工作,下面接着来看看。

3.生成骨架与状态机

编译器在遇到await关键字会自动构建骨架与生成状态机,按照以上例子来看看编译器做的工作有那些。

[DebuggerStepThrough]
private static void <Main>(string[] args)
{
Main(args).GetAwaiter().GetResult();
} [AsyncStateMachine((Type) typeof(<Main>d__0)), DebuggerStepThrough]
private static Task Main(string[] args)
{
<Main>d__0 stateMachine = new <Main>d__0 {
args = args,
<>t__builder = AsyncTaskMethodBuilder.Create(),
<>1__state = -
};
stateMachine.<>t__builder.Start<<Main>d__0>(ref stateMachine);
return stateMachine.<>t__builder.get_Task();
} [AsyncStateMachine((Type) typeof(<SayHi>d__1)), DebuggerStepThrough]
private static Task SayHi()
{
<SayHi>d__1 stateMachine = new <SayHi>d__1 {
<>t__builder = AsyncTaskMethodBuilder.Create(), //如果返回的是void builder为AsyncVoidMethodBuilder
<>1__state = -1 //状态初始化为-1
};
stateMachine.<>t__builder.Start<<SayHi>d__1>(ref stateMachine); //开始执行 传入状态机的引用
return stateMachine.<>t__builder.get_Task(); //返回结果
}

1.编译器会自动生成void mian程序入口方法,它会调用async Task main方法。(所以说c#7.1支持异步main方法,其实只是编译器做了一点小工作)

2.main方法里的输出内容与调用SayHi方法代码消失了,取而代之的是编译器生成了骨架方法,初始化 <Main>d__0 状态机,把状态机的状态字段<>1__state

初始化为-1,builder为AsyncTaskMethodBuilder实例,接着调用builder的Start方法。

3.SayHi方法同2

接着看看AsyncTaskMethodBuilder的Start方法

[DebuggerStepThrough]
public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine: IAsyncStateMachine
{
if (((TStateMachine) stateMachine) == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
}
Thread currentThread = Thread.CurrentThread;
Thread thread2 = currentThread;
ExecutionContext context2 = currentThread._executionContext;
SynchronizationContext context3 = currentThread._synchronizationContext;
try
{
stateMachine.MoveNext(); //调用了状态机的MoveNext方法
}
finally
{
SynchronizationContext context4 = context3;
Thread thread3 = thread2;
if (!ReferenceEquals(context4, thread3._synchronizationContext))
{
thread3._synchronizationContext = context4;
}
ExecutionContext contextToRestore = context2;
ExecutionContext currentContext = thread3._executionContext;
if (!ReferenceEquals(contextToRestore, currentContext))
{
ExecutionContext.RestoreChangedContextToThread(thread3, contextToRestore, currentContext);
}
}
}

Start方法调用了状态机的MoveNext方法,是不是很熟悉?接下来看看状态机长什么样子。

[CompilerGenerated]
private sealed class <Main>d__0 : IAsyncStateMachine
{
// Fields
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
public string[] args;
private TaskAwaiter <>u__1; // Methods
private void MoveNext()
{
int num = this.<>1__state;
try
{
TaskAwaiter awaiter;
if (num == )
{
awaiter = this.<>u__1;
this.<>u__1 = new TaskAwaiter();
this.<>1__state = num = -;
goto TR_0004;
}
else //1: <>1_state初始值为-1,所以先进到该分支,由线程1执行
{
Console.WriteLine($"{(int) Thread.get_CurrentThread().ManagedThreadId}:MainStart"); //标记1 //线程1执行 所以输出 1:MainStart
awaiter = Program.SayHi().GetAwaiter(); //重点:获取Taskd GetAwaiter方法返回TaskAwaiter
if (awaiter.IsCompleted) //重点:判断任务是否已经完成
{
goto TR_0004; //SayHi方法是延时任务,所以正常情况下不会跳进这里
}
else
{
this.<>1__state = num = ; //赋值状态0
this.<>u__1 = awaiter;
Program.<Main>d__0 stateMachine = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Main>d__0>(ref awaiter, ref stateMachine); //重点:把TaskAwaiter与该状态机,线程1执行到这返回
}
}
                 return;
TR_0004:
awaiter.GetResult(); //重点:获取结果 由线程1执行或延时任务不定线程执行
Console.WriteLine($"{(int) Thread.get_CurrentThread().ManagedThreadId}:MainEnd"); //标记4 所以输出 5:MainEnd
                 this.<>1__state = -; this.<>t__builder.SetResult();//设置结果 
}
catch (Exception exception)
{
this.<>1__state = -;
this.<>t__builder.SetException(exception); //设置异常
}
}
[DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } }

上面我圈了重点的是关于Task类型能实现async await的关键操作,

1.线程1执行调用Task实例的GetAwaiter方法返回TaskAwaiter实例。

2.判断TaskAwaiter实例的IsCompleted属性是否完成,如果已完成,跳转到TR_0004,否则执行到AwaitUnsafeOnCompleted方法,线程1结束返回。

我们继续来看看AwaitUnsafeOnCompleted方法,没反编译出来,所以我们来看看与它类似的AwaitOnCompleted方法( AwaitUnsafeOnCompleted实际上会调用UnsafeOnCompleted方法)

public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter: INotifyCompletion where TStateMachine: IAsyncStateMachine
{
try
{
awaiter.OnCompleted(this.GetStateMachineBox<TStateMachine>(ref stateMachine).MoveNextAction);
}
catch (Exception exception1)
{
Task.ThrowAsync(exception1, null);
}
}

看到这里是不是豁然开朗了

1.注册TaskAwaiter实例完成任务的回调方法,等任务完成后将会调用状态机的MoveNext方法,由上篇文章Task的启动方式知道后面的操作将会交由线程池的线程处理。所以标记3跟标记4将会在空闲的线程上执行。

2.<>1__state为0,跳到TR_0004执行,调用TaskAwaiter实例的GetResult()方法,执行await后面的代码,返回结果。

SayHi方法同上。

结论

编译器遇到await后会自动构建骨架与状态机,把await后面的代码挪到任务完成的后面继续执行。主线程第一次调用MoveNext方法时,如果任务已经完成会直接执行后面的操作,否则直接返回,不阻塞主线程的运行。后面的流程

将交由线程池来调度完成。

回到文章开头的问题,什么情况下不适用异步?

可以看出来,使用异步编译器会生成大量额外的操作,而不耗时或者CPU密集型工作使用异步就是添堵。

思考

是不是只有Task才能用async与await?

下一篇我将来探讨一下这个问题,感兴趣的小伙伴可以关注留意后续更新

有说得不对的地方欢迎大神指正,欢迎讨论,共同进步

Async,Await 深入源码解析的更多相关文章

  1. 异步任务spring @Async注解源码解析

    1.引子 开启异步任务使用方法: 1).方法上加@Async注解 2).启动类或者配置类上@EnableAsync 2.源码解析 虽然spring5已经出来了,但是我们还是使用的spring4,本文就 ...

  2. jQuery2.x源码解析(回调篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 通过艾伦的博客,我们能看出,jQuery的pro ...

  3. Koa2 源码解析(1)

    Koa2 源码解析 其实本来不想写这个系列文章的,因为Koa本身很精简,一共就4个文件,千十来行代码. 但是因为想写 egg[1] 的源码解析,而egg是基于Koa2的,所以就先写个Koa2的吧,用作 ...

  4. Ocelot简易教程(七)之配置文件数据库存储插件源码解析

    作者:依乐祝 原文地址:https://www.cnblogs.com/yilezhu/p/9852711.html 上篇文章给大家分享了如何集成我写的一个Ocelot扩展插件把Ocelot的配置存储 ...

  5. .Net Core缓存组件(Redis)源码解析

    上一篇文章已经介绍了MemoryCache,MemoryCache存储的数据类型是Object,也说了Redis支持五中数据类型的存储,但是微软的Redis缓存组件只实现了Hash类型的存储.在分析源 ...

  6. redux的源码解析

    一. redux出现的动机 1. Javascript 需要管理比任何时候都要多的state2. state 在什么时候,由于什么原因,如何变化已然不受控制.3. 来自前端开发领域的新需求4. 我们总 ...

  7. 简单理解 OAuth 2.0 及资料收集,IdentityServer4 部分源码解析

    简单理解 OAuth 2.0 及资料收集,IdentityServer4 部分源码解析 虽然经常用 OAuth 2.0,但是原理却不曾了解,印象里觉得很简单,请求跳来跳去,今天看完相关介绍,就来捋一捋 ...

  8. Redux系列x:源码解析

    写在前面 redux的源码很简洁,除了applyMiddleware比较绕难以理解外,大部分还是 这里假设读者对redux有一定了解,就不科普redux的概念和API啥的啦,这部分建议直接看官方文档. ...

  9. DotNetOpenAuth Part 1 : Authorization 验证服务实现及关键源码解析

    DotNetOpenAuth 是 .Net 环境下OAuth 开源实现框架.基于此,可以方便的实现 OAuth 验证(Authorization)服务.资源(Resource)服务.针对 DotNet ...

随机推荐

  1. Lack of free swap space on Zabbix server

    在模板(Template)里找到Linux OS模板,修改触发器 配置>模板>Template OS Linux>触发器 找到swap关键字 修改 {Template OS Linu ...

  2. 意外发现PHP另一个显示转换类型 binary

    竟然不知道除了(string)$a之外,还有(binary)$a知道unset可以不加括号,但不知道还有这种写法(unset) 请看lex文件(php-7.1.8) <ST_IN_SCRIPTI ...

  3. lly的瞬移方块(并查集)

    lly的瞬移方块 Description llyllylly最近发明了一个叫瞬移方块的游戏,为啥llyllylly这么闲呢,这得从一只蝙蝠说起..... llyllylly决定给大家也分享一下这个游戏 ...

  4. MySQL 同步复制及高可用方案总结

    1.前言 mysql作为应用程序的数据存储服务,要实现mysql数据库的高可用.必然要使用的技术就是数据库的复制,如果主节点出现故障可以手动的切换应用到从节点,这点相信运维同学都是知道,并且可以实现的 ...

  5. CCF题库刷题编译错误

    最近在CCF上刷题,因为C语言更合适,就使用了devc编译器,选择C语言但是却报编译错误 后来查了一下,发现在提交时选择C++语言就能满分通过,问题得以解决.

  6. STM32F103ZET6 PWM输出

    1.通用定时器的PWM功能 STM32F103ZET6有4个通用定时器,分别是TIM2.TIM3.TIM4.TIM5. 通用定时器由一个可编程预分频器驱动的16位自动装载计数器构成. 通用定时器的很多 ...

  7. java对象转为json字符串

    1.使用fastjson开源json工具类库 2.java类未添加get()和set()方法,java对象初始化时,使用fastjson解析,得到的json字符串有时为空{} 3.Java对象转为js ...

  8. [ddt01篇]十年测试老鸟帮您解析:ddt数据驱动实现自动化测试入门基础应用

    一.什么是DDT数据驱动框架 ​ 全称:data driver test数据驱动测试框架,可以完美的应用于unittest框架实现数据驱动.ddt使用简介: 1.测试数据为多个字典的list类型 2. ...

  9. 数据库学习 day1 认识数据库

    从SQL的角度而言,数据库是一个以某种有组织的方式储存的数据集合. 我们可以把它比作一个“文件柜”,这个“文件柜”是一个存放数据的物理位置,不管数据是什么,也不管数据是如何组织的. 下面介绍几个术语 ...

  10. 程序员的 Ubuntu 19.10 配置与优化指南

    原文地址:程序员的 Ubuntu 19.10 配置与优化指南 0x00 环境 CPU: Intel Core i9-9900k GPU: GeForce RTX 2070 SUPER RAM: DDR ...