昨天我正在讨论新的 C#“异步”特性,特别是深入研究生成的代码是什么样的,以及 the GetAwaiter()
/ BeginAwait()
/ EndAwait()
调用。
我们详细查看了 C# 编译器生成的状态机,有两个方面我们无法理解:
为什么生成的类包含一个 Dispose() 方法和一个 $__disposing 变量,它们似乎从未被使用过(并且该类没有实现 IDisposable)。
为什么在调用 EndAwait() 之前内部状态变量设置为 0,而 0 通常看起来意味着“这是初始入口点”。
我怀疑第一点可以通过在异步方法中做一些更有趣的事情来回答,尽管如果有人有任何进一步的信息,我会很高兴听到它。然而,这个问题更多地是关于第二点。
这是一段非常简单的示例代码:
using System.Threading.Tasks;
class Test
{
static async Task<int> Sum(Task<int> t1, Task<int> t2)
{
return await t1 + await t2;
}
}
...这是为实现状态机的 MoveNext()
方法生成的代码。这是直接从 Reflector 复制的——我还没有修复无法描述的变量名称:
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
),它将有效地再次启动异步方法,据我所知,这完全不合适... if 和 MoveNext()
没有被调用,状态的变化是无关紧要的。
这仅仅是编译器为异步重用迭代器块生成代码的副作用,它可能有更明显的解释吗?
重要免责声明
显然这只是一个 CTP 编译器。我完全希望在最终发布之前情况会发生变化——甚至可能在下一个 CTP 发布之前。这个问题绝不是试图声称这是 C# 编译器中的一个缺陷或类似的东西。我只是想弄清楚我是否错过了一个微妙的原因:)
好吧,我终于有一个真正的答案了。我有点自己解决了这个问题,但只有在团队 VB 部分的 Lucian Wischik 确认确实有充分的理由之后。非常感谢他 - 请访问 his blog(在 archive.org 上),它很震撼。
此处的值 0 只是特殊的,因为它不是正常情况下您可能处于 await
之前的有效状态。特别是,这不是状态机最终可能会在其他地方测试的状态。我相信使用任何非正值也可以正常工作:-1 不用于此,因为它逻辑上不正确,因为 -1 通常表示“完成”。我可以争辩说我们现在给状态 0 赋予了额外的含义,但最终它并不重要。这个问题的重点是找出为什么要设置状态。
如果等待以捕获的异常结束,则该值是相关的。我们最终可以再次回到相同的等待语句,但我们不能处于表示“我正要从那个等待中回来”的状态,否则所有类型的代码都会被跳过。用一个例子来说明这一点是最简单的。请注意,我现在使用的是第二个 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
可以是任何可等待的 - 可能是一个任务,也可能是其他东西。就我的测试而言,它总是为 IsCompleted
返回 false,并在 GetResult
中引发异常。
这是为 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
抛出异常时会发生什么。我们将遍历 catch 块,增加 i
,然后再次循环(假设 i
仍然小于 3)。我们仍处于调用 GetResult
之前的任何状态...但是当我们进入 try
块时,我们必须打印“In Try”并再次调用 GetAwaiter
.. . 并且我们只会在 state 不是 1 的情况下这样做。如果没有 state = 0
分配,它将使用现有的 awaiter 并跳过 Console.WriteLine
调用。
这是一段相当曲折的代码,但这只是表明团队必须考虑的事情。我很高兴我不负责实施这个:)
如果它保持在 1(第一种情况),您将收到对 EndAwait
的调用,而没有对 BeginAwait
的调用。如果它保持在 2 (第二种情况),你会在另一个等待者上得到相同的结果。
我猜如果调用 BeginAwait 已经启动(我的猜测),调用它会返回 false,并保留原始值以在 EndAwait 处返回。如果是这种情况,它将正常工作,而如果将其设置为 -1,则第一种情况可能会出现未初始化的 this.<1>t__$await1
。
然而,这假设 BeginAwaiter 实际上不会在第一次调用之后的任何调用上启动操作,并且在这些情况下它将返回 false。开始当然是不可接受的,因为它可能会产生副作用或只是给出不同的结果。它还假设 EndAwaiter 无论被调用多少次都将始终返回相同的值,并且可以在 BeginAwait 返回 false 时调用(根据上述假设)
这似乎是一种防止竞争条件的措施
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>t__$await1。如果状态保持在 1,那么最后一部分将改为:
//second thread
//I suppose this un matched call to EndAwait will fail
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
此外,如果将其设置为 2,则状态机将假定它已经获得了第一个不真实的操作的值,并且(可能)未分配的变量将用于计算结果
这可能与堆叠/嵌套的异步调用有关吗?
IE:
async Task m1()
{
await m2;
}
async Task m2()
{
await m3();
}
async Task m3()
{
Thread.Sleep(10000);
}
在这种情况下,movenext 代表会被多次调用吗?
真的只是一个平底船吗?
MoveNext()
将在它们中的每一个上调用一次。
实际状态说明:
可能的状态:
0 已初始化(我认为是这样)或等待操作结束
>0 刚刚调用 MoveNext,选择下一个状态
-1 结束
这个实现是否有可能只是想确保如果从任何地方发生另一个对 MoveNext 的调用(在等待期间),它将从头开始重新评估整个状态链,以重新评估可能同时已经过时的结果?
不定期副业成功案例分享