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. [vijos1304]回文数<模拟>

    题目链接:https://vijos.org/p/1304 好久没写博客了,最近一直打不出题,感觉自己是废了,今天做了一道模拟水题,但还是半天没过,后来才发现是忘记考虐10以上的进制是带有字母的,然后 ...

  2. ES6规范及语法基础

    var的特点 函数作用域 let的特点 没有变量提升,必须先声明.再调用 同一个作用域下不可以重复定义同一个名称 块级作用域 function fun(){ let a = 10 if(true){ ...

  3. E - River Hopscotch POJ - 3258(二分)

    E - River Hopscotch POJ - 3258 Every year the cows hold an event featuring a peculiar version of hop ...

  4. int不可为null引发的 MyBatis做持久层框架,返回值类型要为Integer问题

    MyBatis做持久层框架,返回值类型要为Integer MyBatis 做持久层时,之前没注意,有时候为了偷懒使用了int类型做为返回的类型,这样是不可取的,MyBatis做持久层框架,返回值类型要 ...

  5. 1030 Travel Plan (30分)(dijkstra 具有多种决定因素)

    A traveler's map gives the distances between cities along the highways, together with the cost of ea ...

  6. 【php】日期时间

    一. 日期时间: a) 这是一块非常重要的内容,我们在windows当中,或者是将来要接触的定时器也好,都是需要使用到这一块内容的!二. PHP当中的日期时间: a) 时间戳:time()可以获取时间 ...

  7. mpvue微信小程序http请求终极解决方案-fly.js

    fly.js是什么? 一个支持所有JavaScript运行环境的基于Promise的.支持请求转发.强大的http请求库.可以让您在多个端上尽可能大限度的实现代码复用(官网解释) fly.js有什么特 ...

  8. android开发对应高德地图定位服务进度一

    进行android的高德地图开发首先需要进入高德地图的控制台进行注册登录.之后创建新的应用并且绑定软件得到相应的key. 这里面需要找到自己软件对应的多个SHA1.这里有发布版和调试版,以及对应的软件 ...

  9. 路由与交换,cisco路由器配置,浮动静态路由

    设置浮动静态路由的目的就是为了防止因为一条线路故障而引起网络故障.言外之意就是说浮动静态路由实际上是主干路由的备份.例如下图: 假如我们设路由器之间的串口(seria)为浮动静态路由(管理距离为100 ...

  10. Kitty-Cloud服务搭建过程剖析

    项目地址 https://github.com/yinjihuan/kitty-cloud 服务搭建 大家目前看到的都是我已经搭建好了的服务,如果让你从零开始自己搭建一个微服务的项目,要怎么做? 我们 ...