上一篇随笔留下了几个问题没能解决:
· 调用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. 终止TTask.Run启动的线程

    unit Unit15; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, Syste ...

  2. IO-MYSQL的理解

    数据库IO简介   IO有四种类型:连续读,随机读,随机写和连续写,连续读写的IO size通常比较大(128KB-1MB),主要衡量吞吐量,而随机读写的IO size比较小(小于8KB),主要衡量I ...

  3. 阻止ARP欺骗

    利用Look N Stop防火墙,防止arp欺骗 阻止网络执法官控制 网络执法官是利用的ARp欺骗的来达到控制目的的. ARP协议用来解析IP与MAC的对应关系,所以用下列方法可以实现抗拒网络执法官的 ...

  4. python点滴:判断字符串是否为合法json格式

    在一些情况下,我们需要判断字符串是否为合法json格式. 思路很简单:尝试对字符串使用json.loads(),如果不是合法json格式,则会抛出ValueError异常. 示例如下: import ...

  5. Vue - iview 开发经验

    Q:打包之后,iview表格宽度异常,过宽或者没有宽度 A:由于columns内某一项width设置为‘百分比(20%)’或者‘100px’导致的, columns内项目的width必须为number ...

  6. 批注@SuppressWarnings 的作用

    J2SE 提供的最后一个批注是 @SuppressWarnings.该批注的作用是给编译器一条指令,告诉它对被批注的代码元素内部的某些警告保持静默. 一点背景:J2SE 5.0 为 Java 语言增加 ...

  7. 大数据-08-Sqoop入门

    简介 Sqoop是一款开源的工具,主要用于在Hadoop(Hive)与传统的数据库(mysql.postgresql-)间进行数据的传递,可以将一个关系型数据库(例如 : MySQL ,Oracle ...

  8. 【转】Principles of training multi-layer neural network using backpropagation

    Principles of training multi-layer neural network using backpropagation http://galaxy.agh.edu.pl/~vl ...

  9. elasticsearch index tuning

    一.扩容 tag_server当前使用ElasticSearch版本为5.6,此版本单个index的分片是固定的,一旦创建后不能更改. 1.扩容方法1,不适 ES6.1支持split index功能, ...

  10. 运放积分电路MULTISIM

    有些需要反馈回路