上一篇随笔留下了几个问题没能解决:
· 调用IAsyncStateMachine.MoveNext方法的线程何时发起的?
· lambda的执行为何先于MoveNext方法?
· 后执行的MoveNext方法做了些什么事情?

那么今天就来尝试解决它们吧~
PS: 本文中部分代码来自上一篇随笔,具体来源可参考注释中的章节标题

一、哪里来的线程?

通过上一篇随笔的调查我们知道了,async标记的方法的方法体会被编译到一个内部结构体的MoveNext方法中,并且也找到了MoveNext的调用者,再且也证实了有两个调用者是来自于主线程之外的同一个工作线程。
可是这一个线程是何时发起的呢?上一次调查时没能找到答案,这一次就继续从MoveNext方法开始,先找找看Task相关的操作有哪些。

 1 // 三、理解await
bool '<>t__doFinallyBodies';
Exception '<>t__ex';
int CS$$;
TaskAwaiter<string> CS$$;
TaskAwaiter<string> CS$$; try
{
'<>t__doFinallyBodies' = true;
CS$$ = this.'<>1__state';
if (CS$$ != )
{
CS$$ = this.'<>4__this'.GetHere().GetAwaiter();
if (!CS$$.IsCompleted)
{
this.'<>1__state' = ;
this.'<>u__$awaiter1' = CS$$;
this.'<>t__builder'.AwaitUnsafeOnCompleted(ref CS$$, ref this);
'<>t__doFinallyBodies' = false;
return;
}
}
else
{
CS$$ = this.'<>u__$awaiter1';
this.'<>u__$awaiter1' = CS$$;
this.'<>1__state' = -;
} Console.WriteLine(CS$$.GetResult());
}

注意到14行的GetHere方法返回了一个Task<string>,随后的GetAwaiter返回的是TaskAwaiter<string>。
不过这两个Get方法都没有做什么特别的处理,那么就看看接下来是谁使用了TaskAwaiter<string>实例。
于是就来看看19行的AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted里面做了些什么吧。

 // System.Runtime.CompilerServices.AsyncVoidMethodBuilder
[__DynamicallyInvokable, SecuritySafeCritical]
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
try
{
Action completionAction = this.m_coreState
.GetCompletionAction<AsyncVoidMethodBuilder, TStateMachine>(ref this, ref stateMachine);
awaiter.UnsafeOnCompleted(completionAction);
}
catch (Exception exception)
{
AsyncMethodBuilderCore.ThrowAsync(exception, null);
}
}

这里主要做了两件事:
一是创建了一个Action,MoveNext方法的信息已经随着stateMachine被封装进去了。
二是把上面这个Action交给Awaiter,让它在await的操作完成后执行这个Action。

先来看看Action的构建细节吧:

 // System.Runtime.CompilerServices.AsyncMethodBuilderCore
[SecuritySafeCritical]
internal Action GetCompletionAction<TMethodBuilder, TStateMachine>(ref TMethodBuilder builder, ref TStateMachine stateMachine)
where TMethodBuilder : IAsyncMethodBuilder
where TStateMachine : IAsyncStateMachine
{
7 Debugger.NotifyOfCrossThreadDependency();
8 ExecutionContext executionContext = ExecutionContext.FastCapture();
9 Action action;
10 AsyncMethodBuilderCore.MoveNextRunner moveNextRunner;
11 if (executionContext != null && executionContext.IsPreAllocatedDefault)
12 {
13 action = this.m_defaultContextAction;
14 if (action != null)
{
return action;
}
18 moveNextRunner = new AsyncMethodBuilderCore.MoveNextRunner(executionContext);
19 action = new Action(moveNextRunner.Run);
20 if (AsyncCausalityTracer.LoggingOn)
21 {
22 action = (this.m_defaultContextAction = this.OutputAsyncCausalityEvents<TMethodBuilder>(ref builder, action));
23 }
else
{
this.m_defaultContextAction = action;
}
}
else
{
moveNextRunner = new AsyncMethodBuilderCore.MoveNextRunner(executionContext);
action = new Action(moveNextRunner.Run);
if (AsyncCausalityTracer.LoggingOn)
{
action = this.OutputAsyncCausalityEvents<TMethodBuilder>(ref builder, action);
}
}
38 if (this.m_stateMachine == null)
39 {
40 builder.PreBoxInitialization<TStateMachine>(ref stateMachine);
41 this.m_stateMachine = stateMachine;
42 this.m_stateMachine.SetStateMachine(this.m_stateMachine);
43 }
44 moveNextRunner.m_stateMachine = this.m_stateMachine;
45 return action;
}

这段的分支有点多,行号上的标记是我DEBUG时经过的分支。
可以看到,这个方法里面出现了MoveNext方法的调用者MoveNextRunner,它的Run方法被封装到了返回的Action里。
也就是说,只要这个Action被执行,就会进入Run方法,而Run方法里面有两条分支,简单来说就是:
1.直接调用MoveNext
2.通过InvokeMoveNext调用MoveNext

第40行的赋值不影响Action中的Run,只是在头尾追加了状态记录的操作。
接下来就赶紧找一找执行这个Action的地方吧!
深入UnsafeOnCompleted方法,最终可以找到如下的方法,第一个参数就是要跟踪的对象:

 // System.Threading.Tasks.Task
[SecurityCritical]
internal void SetContinuationForAwait(
Action continuationAction,
bool continueOnCapturedContext,
bool flowExecutionContext,
ref StackCrawlMark stackMark)
{
9 TaskContinuation taskContinuation = null;
10 if (continueOnCapturedContext)
11 {
12 SynchronizationContext currentNoFlow = SynchronizationContext.CurrentNoFlow;
13 if (currentNoFlow != null && currentNoFlow.GetType() != typeof(SynchronizationContext))
{
taskContinuation = new SynchronizationContextAwaitTaskContinuation(
currentNoFlow, continuationAction, flowExecutionContext, ref stackMark);
}
18 else
19 {
20 TaskScheduler internalCurrent = TaskScheduler.InternalCurrent;
21 if (internalCurrent != null && internalCurrent != TaskScheduler.Default)
{
taskContinuation = new TaskSchedulerAwaitTaskContinuation(
internalCurrent, continuationAction, flowExecutionContext, ref stackMark);
}
26 }
27 }
28 if (taskContinuation == null && flowExecutionContext)
{
taskContinuation = new AwaitTaskContinuation(continuationAction, true, ref stackMark);
}
32 if (taskContinuation != null)
{
if (!this.AddTaskContinuation(taskContinuation, false))
{
taskContinuation.Run(this, false);
return;
}
}
40 else if (!this.AddTaskContinuation(continuationAction, false))
41 {
42 AwaitTaskContinuation.UnsafeScheduleAction(continuationAction, this);
43 }
}

同样的,行号的标记意味着经过的分支。继续跟进:

 // System.Threading.Tasks.AwaitTaskContinuation
[SecurityCritical]
internal static void UnsafeScheduleAction(Action action, Task task)
{
AwaitTaskContinuation awaitTaskContinuation = new AwaitTaskContinuation(action, false);
TplEtwProvider log = TplEtwProvider.Log;
if (log.IsEnabled() && task != null)
{
awaitTaskContinuation.m_continuationId = Task.NewId();
log.AwaitTaskContinuationScheduled(
(task.ExecutingTaskScheduler ?? TaskScheduler.Default).Id,
task.Id,
awaitTaskContinuation.m_continuationId);
}
ThreadPool.UnsafeQueueCustomWorkItem(awaitTaskContinuation, false);
}
 // System.Threading.ThreadPool
[SecurityCritical]
internal static void UnsafeQueueCustomWorkItem(IThreadPoolWorkItem workItem, bool forceGlobal)
{
ThreadPool.EnsureVMInitialized();
try
{
}
finally
{
ThreadPoolGlobals.workQueue.Enqueue(workItem, forceGlobal);
}
}

这里出现了全局线程池,然而没有找到MSDN对ThreadPoolGlobals的解释,这里头的代码又实在太多了。。。暂且模拟一下看看:

 Console.WriteLine("HERE");
var callback = new WaitCallback(state => Println("From ThreadPool"));
ThreadPool.QueueUserWorkItem(callback);
Console.WriteLine("THERE");

QueueUserWorkItem方法内部调用了ThreadPoolGlobals.workQueue.Enqueue,运行起来效果是这样的:

HERE
THERE
From ThreadPool

再看看线程信息:

Function: CsConsole.Program.Main(), Thread: 0x2E58 主线程
Function: CsConsole.Program.Main(), Thread: 0x2E58 主线程
Function: CsConsole.Program.Main.AnonymousMethod__6(object), Thread: 0x30EC 工作线程

和async的表现简直一模一样是不是~?从调用堆栈也可以看到lambda的执行是源于这个workQueue:

到此为止算是搞定第一个问题了。

二、lambda为何先行?

先来回忆一下GetHere方法的内容:

// 三、理解await
Task<string> GetHere()
{
return Task.Run(() =>
{
Thread.Sleep();
return "HERE";
});
}

要追踪的lambda就是在这里构造的,而调用GetHere的地方也只有一个,就是MoveNext方法的try块。
而MoveNext的调用方也都找出来了:

其中Start方法是在主线程中调用的,可以由SampleMethod追溯到。那么以下的调用信息:

Function: Test.Program.Main(string[]), Thread: 0xE88 主线程
Function: Test.Program.GetHere.AnonymousMethod__3(), Thread: 0x37DC 工作线程
Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run(), Thread: 0x37DC 工作线程
Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object), Thread: 0x37DC 工作线程

这个顺序不是有点奇怪吗?lambda怎么能先于MoveNextRunner的两个方法执行?
其实我在这里犯了一个很明显的思维错误。。。Start调用来自主线程,lambda调用来自子线程,于是直觉性地否定了它们之间的关联。。。
很显然,整个过程其实应该是这样的:
1. 主线程:Start方法调用了MoveNext,MoveNext调用了GetHere
2. 主线程:GetHere方法返回了包含lambda信息的Task
3. 主线程:Task经过变换与包装,最终进入了线程池
4. 子线程:通过Task调用了lambda
5. 子线程:通过Runner调用了MoveNext

子线程中的lambda是来源于主线程第一次调用的MoveNext,和之后的Run啊InvokeMoveNext是没有关系的,所以这个顺序也就不奇怪了。
通过DEBUG几个关键点即可以验证这一顺序。第二个也算搞定了。

三、MoveNext干了什么?

第二个问题虽然解决了,但是也让第三个问题显得更加重要,既然lambda确实是先于MoveNext,那么MoveNext到底做了些什么?
通过之前的调查,现在知道了:
1. MoveNext在lambda执行之前被Start方法在主线程调用了一次,过程中把lambda封送给了线程池
2. MoveNext在lambda执行之后被InvokeMoveNext又调用了一次,这一次做了什么处理是尚不明了的

回头看本文的第一段代码,前后两次进入同一段代码,但是做了不同的事情,那么显然就是两次走了不同的分支咯。
由于这段代码本身是DEBUG不进去的,所以只能在其内部调用的方法里断点了。我打了如下几个断点:
· Task<TResult>.GetAwaiter
· AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted
· TaskAwaiter<TResult>.GetResult
· Program.SampleMethod
· MoveNextRunner.InvokeMoveNext

来看看执行结果如何吧:

Function: Test.Program.SampleMethod(), Thread: 0x9BC 主线程
Function: System.Threading.Tasks.Task<TResult>.GetAwaiter(), Thread: 0x9BC 主线程
Function: System.Runtime.CompilerServices.AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted<TAwaiter,TStateMachine>(ref TAwaiter, ref TStateMachine), Thread: 0x9BC 主线程
Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object), Thread: 0x3614 工作线程
Function: System.Runtime.CompilerServices.TaskAwaiter<TResult>.GetResult(), Thread: 0x3614 工作线程

需要注意的是,断到InvokeMoveNext里头的时候,只有这一行代码:

((IAsyncStateMachine)stateMachine).MoveNext();

而当我按下F11步入之后,可以猜一猜跳到了哪:

async void SampleMethod()
{
Console.WriteLine(await GetHere());
}

而在这个时候GetResult还没执行到。
由此可以整理出try块里的执行过程如下:

 try
{
3 '<>t__doFinallyBodies' = true;
4 CS$$ = this.'<>1__state';
5 if (CS$$ != )
{
CS$$ = this.'<>4__this'.GetHere().GetAwaiter();
if (!CS$$.IsCompleted)
{
this.'<>1__state' = ;
this.'<>u__$awaiter1' = CS$$;
this.'<>t__builder'.AwaitUnsafeOnCompleted(ref CS$$, ref this);
'<>t__doFinallyBodies' = false;
return;
}
}
17 else
18 {
19 CS$$ = this.'<>u__$awaiter1';
20 this.'<>u__$awaiter1' = CS$$;
21 this.'<>1__state' = -;
22 }
23
24 Console.WriteLine(CS$$.GetResult());
}

红字是第一次经过的分支,黄底是第二次经过的分支。
而前面说到的F11进入的区块,实际上就是这里的第24行。
所以现在可以知道,第二次MoveNext做了什么:
执行async方法中await后的代码。

四、水落石出

async和await的轮廓逐渐清晰了~再结合上一篇的一段代码来看看:

// 二、理解async
void MoveNext()
{
bool local0;
Exception local1; try
{
local0 = true;
Thread.Sleep(1000);
Console.WriteLine("HERE");
}
catch (Exception e)
{
local1 = e;
this.'<>1__state' = -;
this.'<>t__builder'.SetException(local1);
return;
} this.'<>1__state' = -;
this.'<>t__builder'.SetResult()
}

黄底的两句代码原本是在哪的还记得吗?看这里:

// 二、理解async
async void SampleMethod()
{
Thread.Sleep();
Console.WriteLine("HERE");
}

因为这个async方法中没有出现await调用,所以可以认为仅有的两句代码是出现在await操作之前。
再让SampleMethod变成这样:

async void SampleMethod()
{
Console.WriteLine("WHERE");
Console.WriteLine(await GetHere());
}

再看看现在的MoveNext方法:

 try
{
'<>t__doFinallyBodies' = true;
CS$$ = this.'<>1__state';
if (CS$$ != )
{
Console.WriteLine("WHERE");
CS$$ = this.'<>4__this'.GetHere().GetAwaiter();
if (!CS$$.IsCompleted)
{
this.'<>1__state' = ;
this.'<>u__$awaiter1' = CS$$;
this.'<>t__builder'.AwaitUnsafeOnCompleted(ref CS$$, ref this);
'<>t__doFinallyBodies' = false;
return;
}
}
else
{
CS$$ = this.'<>u__$awaiter1';
this.'<>u__$awaiter1' = CS$$;
this.'<>1__state' = -;
} Console.WriteLine(CS$$.GetResult());
}

这样就可以很明显的看出来await前后的代码被放到了两个区块里,而这两个区块,也就是之前看到的两次执行MoveNext走过的分支。

最终调查结果如下
1. async方法中的代码会被移交给IAsyncStateMachine的MoveNext方法
2. async方法中await操作前后的代码被分离
3. 主线程直接执行await前的代码,并将await的Task移交给线程池ThreadPoolGlobal
4. 子线程执行完主线程递交来的Task后,再次走入MoveNext方法,执行await后的代码

最后想说的是:
这一阵在办公积金销户提取,整个过程就像是个async方法,把申请提交给管理中心(await前操作)以后就得开始等待(await)他们对申请进行审核(执行Task),这个过程加上周末得整整五天,之后还得去管理中心取款(await后操作),总之就是麻烦死了。。。

[C#]async和await刨根问底的更多相关文章

  1. 【转】剖析异步编程语法糖: async和await

    一.难以被接受的async 自从C#5.0,语法糖大家庭又加入了两位新成员: async和await. 然而从我知道这两个家伙之后的很长一段时间,我甚至都没搞明白应该怎么使用它们,这种全新的异步编程模 ...

  2. [C#]剖析异步编程语法糖: async和await

    一.难以被接受的async 自从C#5.0,语法糖大家庭又加入了两位新成员: async和await. 然而从我知道这两个家伙之后的很长一段时间,我甚至都没搞明白应该怎么使用它们,这种全新的异步编程模 ...

  3. 探索c#之Async、Await剖析

    阅读目录: 基本介绍 基本原理剖析 内部实现剖析 重点注意的地方 总结 基本介绍 Async.Await是net4.x新增的异步编程方式,其目的是为了简化异步程序编写,和之前APM方式简单对比如下. ...

  4. c#之Async、Await剖析

    c#之Async.Await剖析 探索c#之Async.Await剖析 2015-06-15 08:35 by 蘑菇先生, 1429 阅读, 5 评论, 收藏, 编辑 阅读目录: 基本介绍 基本原理剖 ...

  5. 为什么我们要使用Async、Await关键字

    前不久,在工作中由于默认(xihuan)使用Async.Await关键字受到了很多质问,所以由此引发这篇博文“为什么我们要用Async/Await关键字”,请听下面分解: Async/Await关键字 ...

  6. [译] C# 5.0 中的 Async 和 Await (整理中...)

    C# 5.0 中的 Async 和 Await [博主]反骨仔 [本文]http://www.cnblogs.com/liqingwen/p/6069062.html 伴随着 .NET 4.5 和 V ...

  7. Async和Await异步编程的原理

    1. 简介 从4.0版本开始.NET引入并行编程库,用户能够通过这个库快捷的开发并行计算和并行任务处理的程序.在4.5版本中.NET又引入了Async和Await两个新的关键字,在语言层面对并行编程给 ...

  8. 异步方法的意义何在,Async和await以及Task的爱恨情仇,还有多线程那一家子。

    前两天刚感受了下泛型接口的in和out,昨天就开始感受神奇的异步方法Async/await,当然顺路也看了眼多线程那几个.其实多线程异步相关的类单个用法和理解都不算困难,但是异步方法Async/awa ...

  9. 多线程之异步编程: 经典和最新的异步编程模型,async与await

    经典的异步编程模型(IAsyncResult) 最新的异步编程模型(async 和 await) 将 IAsyncInfo 转换成 Task 将 Task 转换成 IAsyncInfo 示例1.使用经 ...

随机推荐

  1. Allocation-Free Collections

    假设你有一个方法,通过创建临时的List来收集某些数据,并根据这些数据来统计信息,然后销毁这个临时列表.这个方法被经常调用,导致大量内存分配和释放以及增加的内存碎片.此外,所有这些内存管理都需要时间, ...

  2. java学习笔记17(Calendarl类)

    Calendar类:(日历) 用法:Calendar是一个抽象类:不能实例化(不能new),使用时通过子类完成实现,不过这个类不需要创建子类对象,而是通过静态方法直接获取: 获取对象方法:getIns ...

  3. 若sql语句中order by指定了多个字段,怎么排序?

    举个例子吧: order by id desc,time desc 先是按 id 降序排列 (优先)如果 id 字段 有些是一样的话 再按time 降序排列 (前提是满足id降序排列)

  4. python day02 作业答案

    1. (1).false   (2).false 2. (1).8  (2).4 3. (1).6  (2).3  (3).false (4).3   (5).true   (6).true  (7) ...

  5. DOM树中节点与节点之间的关系图

    获取子代元素节点:children 获取所有子代元素节点数:**.childElementCount  或者  **.children.length

  6. puppet确保程序运行

    exec { 'keep-nginx-running' : user => 'root', unless => 'ps -x | grep nginx|grep -v grep', com ...

  7. linux修改ssh端口 以及禁止root远程登录 (实验机 CentOs)

    把ssh默认远程连接端口修改为3333 1.编辑防火墙配置: vi /etc/sysconfig/iptables 防火墙新增端口3333,方法如下: -A INPUT -m state --stat ...

  8. php防止sql注入的方法(转)

    [一.在服务器端配置] 安全,PHP代码编写是一方面,PHP的配置更是非常关键. 我们php手手工安装的,php的默认配置文件在 /usr/local/apache2/conf/php.ini,我们最 ...

  9. markdown使用问题

    1.配置自定义的markdown.css https://github.com/sameer1994kiki/markdown-css 2.代码块 一行`` 多行 ``` <code>&l ...

  10. 动态开点线段树(陕西师范18k题)---get new skill

    思想: 每次开点的时候:左右孩子都开辟新空间 注意懒惰标记tag: 因为会向下传递        提前在值中减去懒惰标记,避免重复计算 链接:https://www.nowcoder.com/acm/ ...