昨天我正在讨论新的 C#“异步”特性,特别是深入研究生成的代码是什么样的,以及the GetAwaiter()/ BeginAwait()/EndAwait()调用。
the GetAwaiter()
BeginAwait()
EndAwait()
我们详细查看了 C# 编译器生成的状态机,有两个方面我们无法理解:
Dispose()
$__disposing``IDisposable
state
我怀疑第一点可以通过在异步方法中做一些更有趣的事情来回答,尽管如果有人有任何进一步的信息,我会很高兴听到它。然而,这个问题更多地是关于第二点。
这是一段非常简单的示例代码:
using System.Threading.Tasks; class Test { static async Task<int> Sum(Task<int> t1, Task<int> t2) { return await t1 + await t2; } }
MoveNext()…这是为实现状态机的方法生成的代码。这是直接从 Reflector 复制的——我还没有修复无法描述的变量名称:
MoveNext()
public void MoveNext() { try { this.$__doFinallyBodies = true; switch (this.<>1__state) { case 1: break; case 2: goto Label_00DA; case -1: return; default: this.<a1>t__$await2 = this.t1.GetAwaiter<int>(); this.<>1__state = 1; this.$__doFinallyBodies = false; if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)) { return; } this.$__doFinallyBodies = true; break; } this.<>1__state = 0; this.<1>t__$await1 = this.<a1>t__$await2.EndAwait(); this.<a2>t__$await4 = this.t2.GetAwaiter<int>(); this.<>1__state = 2; this.$__doFinallyBodies = false; if (this.<a2>t__$await4.BeginAwait(this.MoveNextDelegate)) { return; } this.$__doFinallyBodies = true; Label_00DA: this.<>1__state = 0; this.<2>t__$await3 = this.<a2>t__$await4.EndAwait(); this.<>1__state = -1; this.$builder.SetResult(this.<1>t__$await1 + this.<2>t__$await3); } catch (Exception exception) { this.<>1__state = -1; this.$builder.SetException(exception); } }
它很长,但这个问题的重要内容是:
// End of awaiting t1 this.<>1__state = 0; this.<1>t__$await1 = this.<a1>t__$await2.EndAwait(); // End of awaiting t2 this.<>1__state = 0; this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
在这两种情况下,状态都会在下一次明显观察到之前再次发生变化......那么为什么将其设置为 0 呢?如果MoveNext()此时再次调用(直接或通过Dispose),它将有效地再次启动异步方法,据我所知,这将是完全不合适的......如果并且MoveNext() 没有 被调用,状态的变化是无关紧要的。
Dispose
这仅仅是编译器为异步重用迭代器块生成代码的副作用,它可能有更明显的解释吗?
重要免责声明
显然这只是一个 CTP 编译器。我完全希望在最终发布之前情况会发生变化——甚至可能在下一个 CTP 发布之前。这个问题绝不是试图声称这是 C# 编译器中的一个缺陷或类似的东西。我只是想弄清楚我是否错过了一个微妙的原因:)
好吧,我终于有一个真正的答案了。我有点自己解决了这个问题,但只有在团队 VB 部分的 Lucian Wischik 确认确实有充分的理由之后。非常感谢他 - 请访问他的博客(在archive.org上),它很震撼。
这里的值 0 只是特殊的,因为它 不是await正常情况下您可能处于的有效状态。特别是,这不是状态机最终可能会在其他地方测试的状态。我相信使用任何非正值也可以正常工作:-1 不用于此,因为它在 逻辑上 不正确,因为 -1 通常表示“完成”。我可以争辩说我们现在给状态 0 赋予了额外的含义,但最终它并不重要。这个问题的重点是找出为什么要设置状态。
await
如果等待以捕获的异常结束,则该值是相关的。我们最终可以再次回到相同的等待语句,但我们 不能 处于表示“我正要从那个等待中回来”的状态,否则所有类型的代码都会被跳过。用一个例子来说明这一点是最简单的。请注意,我现在使用的是第二个 CTP,因此生成的代码与问题中的代码略有不同。
这是异步方法:
static async Task<int> FooAsync() { var t = new SimpleAwaitable(); for (int i = 0; i < 3; i++) { try { Console.WriteLine("In Try"); return await t; } catch (Exception) { Console.WriteLine("Trying again..."); } } return 0; }
从概念上讲,它SimpleAwaitable可以是任何可等待的——也许是一个任务,也许是别的东西。就我的测试而言,它总是返回 false IsCompleted,并在GetResult.
SimpleAwaitable
IsCompleted
GetResult
这是生成的代码MoveNext:
MoveNext
public void MoveNext() { int returnValue; try { int num3 = state; if (num3 == 1) { goto Label_ContinuationPoint; } if (state == -1) { return; } t = new SimpleAwaitable(); i = 0; Label_ContinuationPoint: while (i < 3) { // Label_ContinuationPoint: should be here try { num3 = state; if (num3 != 1) { Console.WriteLine("In Try"); awaiter = t.GetAwaiter(); if (!awaiter.IsCompleted) { state = 1; awaiter.OnCompleted(MoveNextDelegate); return; } } else { state = 0; } int result = awaiter.GetResult(); awaiter = null; returnValue = result; goto Label_ReturnStatement; } catch (Exception) { Console.WriteLine("Trying again..."); } i++; } returnValue = 0; } catch (Exception exception) { state = -1; Builder.SetException(exception); return; } Label_ReturnStatement: state = -1; Builder.SetResult(returnValue); }
我不得不移动Label_ContinuationPoint以使其成为有效代码 - 否则它不在goto语句的范围内 - 但这不会影响答案。
Label_ContinuationPoint
goto
想想GetResult抛出异常时会发生什么。我们将遍历 catch 块、增量i,然后再次循环(假设i仍然小于 3)。我们仍然处于GetResult调用之前的任何状态......但是当我们进入try块内时,我们 必须 打印“In Try”并GetAwaiter再次调用......并且我们只会在 state 不是 1 时这样做。没有state = 0分配,它将使用现有的等待者并跳过调用Console.WriteLine。
i
try
GetAwaiter
state = 0
Console.WriteLine
这是一段相当曲折的代码,但这只是表明团队必须考虑的事情。我很高兴我不负责实施这个:)