C#5asynchronousCTP:为什么在EndAwait调用之前生成的代码中内部“状态”设置为0?

昨天,我正在谈论新的C#“asynchronous”function,特别是探究生成的代码的样子,以及the GetAwaiter() / BeginAwait() / EndAwait()调用。

我们在C#编译器生成的状态机上查看了一些细节,有两个方面我们不明白:

  • 为什么生成的类包含一个Dispose()方法和一个$__disposingvariables,它们似乎永远不会被使用(并且该类不会实现IDisposable )。
  • 为什么在调用EndAwait()之前,内部statevariables设置为0,通常0表示“这是初始入口点”。

我怀疑第一点可以通过在asynchronous方法中做一些更有趣的事情来回答,虽然如果有人有任何进一步的信息,我会很高兴听到它。 然而,这个问题更多的是关于第二点。

这是一个非常简单的示例代码:

 using System.Threading.Tasks; class Test { static async Task<int> Sum(Task<int> t1, Task<int> t2) { return await t1 + await t2; } } 

…这是为实现状态机的MoveNext()方法生成的代码。 这是从reflection器直接复制 – 我还没有解决难以形容的variables名称:

 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呢? 如果在这一点(直接或通过Dispose )再次MoveNext() ,它将再次有效地启动asynchronous方法,这是完全不合适的,据我所知…如果调用MoveNext()国家的变化是无关紧要的。

这只是一个副作用的编译器重复使用迭代器块生成代码的asynchronous,它可能有一个更明显的解释?

重要的免责声明

显然这只是一个CTP编译器。 我完全希望事情在最终发布之前有所改变 – 甚至可能在下一个CTP发布之前。 这个问题绝不会试图声称这是C#编译器或类似的东西的缺陷。 我只是试图找出是否有一个微妙的原因,我错过了:)

好的,我终于有了一个真正的答案。 我自己做了一些工作,但是直到队伍的VB部分的Lucian Wischik证实确实有一个很好的理由。 非常感谢他 – 请访问他的博客 ,这是岩石。

这里的值0只是特殊的,因为它不是一个有效的状态,你可能会在正常情况下await之前。 特别是,这不是国家机器最终可能在别处testing的状态。 我相信,使用任何非正值都可以同样如此:-1不用于这个,因为它在逻辑上是不正确的,因为-1通常意味着“完成”。 我可以争辩说,我们现在赋予0的额外含义,但最终它并不重要。 这个问题的重点在于为什么要设置国家。

如果await在被捕获的exception中结束,则该值是相关的。 我们可以再次回到同样的等待声明中去,但是我们不能处于“我即将从这个等待中回来”的状态,否则所有的代码都会被跳过。 用一个例子来展示这个最简单。 请注意,我现在正在使用第二个CTP,所以生成的代码与问题中的稍有不同。

这里是asynchronous方法:

 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可以是任何等待的 – 也许是一个任务,也许是别的。 对于我的testing,它总是返回false为IsCompleted ,并在GetResult引发exception。

以下是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语句的范围内 – 但是这不会影响答案。

想想当GetResult引发exception时会发生什么。 我们将通过catch块,增加i ,然后再循环(假设i仍然小于3)。 我们仍处于GetResult调用之前的任何状态……但是当我们进入try块时,我们必须打印“In Try”并再次调用GetAwaiter …而且只有在状态不是1.如果没有state = 0赋值,它将使用现有的awaiter并跳过Console.WriteLine调用。

这是一个相当曲折的代码工作,但这只是为了展示团队必须考虑的事情。 我很高兴我不负责执行此:)

如果它保持在1(第一种情况),您将得到一个呼叫EndAwait而不会调用BeginAwait 。 如果它保持在2(第二种情况),你会得到相同的结果只是在另一个awaiter。

我猜测,如果BeginAwait已经启动(从我身边猜测),则返回false,并保持原始值在EndAwait处返回。 如果是这种情况,它会正常工作,而如果你将它设置为-1,你可能会有一个未初始化的this.<1>t__$await1第一种情况。

然而,这假设BeginAwaiter在第一个之后不会实际开始任何调用,并且在这些情况下它将返回false。 当然起点是不可接受的,因为它可能有副作用或者只是给出不同的结果。 它还假设EndAwaiter将始终返回相同的值,无论它被调用多less次,并且可以在BeginAwait返回false时被调用(按照上述假设)

这似乎是一个反对竞争条件的警惕如果我们在状态= 0之后,在一个不同的线程中将movenext称为movenext,那么这个语句会像下面那样

 this.<a1>t__$await2 = this.t1.GetAwaiter<int>(); this.<>1__state = 1; this.$__doFinallyBodies = false; this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate) this.<>1__state = 0; //second thread this.<a1>t__$await2 = this.t1.GetAwaiter<int>(); this.<>1__state = 1; this.$__doFinallyBodies = false; this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate) this.$__doFinallyBodies = true; this.<>1__state = 0; this.<1>t__$await1 = this.<a1>t__$await2.EndAwait(); //other thread this.<1>t__$await1 = this.<a1>t__$await2.EndAwait(); 

如果上面的假设是正确的,那么就有一些不必要的工作,比如得到锯木厂,并重新分配相同的价值来等待1。 如果国家保持在1,那么最后一部分将取代:

 //second thread //I suppose this un matched call to EndAwait will fail this.<1>t__$await1 = this.<a1>t__$await2.EndAwait(); 

进一步,如果它被设置为2,那么状态机将假定它已经获得了第一个将是不真实的动作的值,并且将使用(可能)未分配的variables来计算结果

这可能是堆叠/嵌套的asynchronous调用?

即:

 async Task m1() { await m2; } async Task m2() { await m3(); } async Task m3() { Thread.Sleep(10000); } 

在这种情况下,movenext委托会被多次调用吗?

只是平底船真的吗?

实际状态的解释:

可能的状态:

  • 0初始化(我认为是) 等待操作结束
  • > 0刚刚调用MoveNext,select下一个状态
  • -1结束

这个实现是否有可能只是想保证,如果另一个CallNext MoveNext(等待),它会重新从头评估整个状态链, 重新评估可能已经过时的结果?