C#5异步CTP:为什么在EndAwait调用之前生成的代码中内部"state"设置为0?

Jon*_*eet 193 c# asynchronous c#-5.0

昨天我正在讨论新的C#"异步"功能,特别是深入研究生成的代码的样子和the GetAwaiter()/ BeginAwait()/ EndAwait()调用.

我们在C#编译器生成的状态机中查看了一些细节,有两个方面我们无法理解:

  • 为什么生成的类包含一个永远不会被使用的Dispose()方法和$__disposing变量(并且该类没有实现IDisposable).
  • 为什么state在任何调用之前将内部变量设置为0 EndAwait(),当0通常表示"这是初始入口点"时.

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

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

using System.Threading.Tasks;

class Test
{
    static async Task<int> Sum(Task<int> t1, Task<int> t2)
    {
        return await t1 + await t2;
    }
}
Run Code Online (Sandbox Code Playgroud)

...这里是为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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这很长,但这个问题的重点是这些:

// 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();
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,状态在之后明显被观察之前再次改变......那么为什么要将它设置为0呢?如果MoveNext()此时再次被调用(直接或通过Dispose),它将有效地再次启动异步方法,这是完全不合适的,据我所知...如果和MoveNext() 未被调用,状态的变化是无关紧要的.

这只是编译器重复使用迭代器块生成代码进行异步的副作用,它可能有更明显的解释吗?

重要免责声明

显然这只是一个CTP编译器.我完全希望在最终版本发布前有所改变 - 甚至可能在下一次CTP发布之前.这个问题决不会试图声称这是C#编译器中的一个缺陷或类似的东西.我只想弄清楚是否有一个微妙的原因,我错过了:)

Jon*_*eet 70

好的,我终于有了一个真正的答案.我有点自己解决这个问题,但只有在团队的VB部分的Lucian Wischik证实确实有充分的理由之后.非常感谢他 - 请访问他的博客.

这里的值0只是特殊的,因为它不是一个有效的状态,你可能await在正常情况之前.特别是,它不是状态机最终可能在其他地方进行测试的状态.我相信使用任何非正值都可以正常工作:-1不用于此,因为它在逻辑上是不正确的,因为-1通常意味着"完成".我可以说我们现在给0状态赋予额外的意义,但最终它并不重要.这个问题的关键在于找出为什么要建立国家.

如果await在捕获的异常中结束,则该值是相关的.我们最终可能会再次回到相同的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;
}
Run Code Online (Sandbox Code Playgroud)

从概念上讲,SimpleAwaitable可以是任何等待的 - 也许是一项任务,也许是别的东西.出于我的测试的目的,它总是返回false IsCompleted,并抛出异常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);
}
Run Code Online (Sandbox Code Playgroud)

我不得不移动Label_ContinuationPoint使它成为有效的代码 - 否则它不在goto声明的范围内- 但这不会影响答案.

想想GetResult抛出异常时会发生什么.我们将通过catch块,递增i,然后再循环(假设i仍然小于3).我们仍然处于GetResult通话之前的状态......但是当我们进入try区块时,我们必须打印"In Try"并GetAwaiter再次打电话......如果状态不是,我们只会这样做.在state = 0分配时,将使用现有的awaiter并跳过Console.WriteLine调用.

这是一个相当曲折的代码,但这只是为了展示团队必须考虑的事情.我很高兴我不负责实施这个:)

  • @Shekhar_Pro:在手动编写的代码中,它是 - 因为它使代码难以阅读和遵循.没有人会读取自动生成的代码,除了像我这样反编译的傻瓜:) (12认同)
  • @Shekhar_Pro:是的,这是一个转到.你应该*期望*在自动生成的状态机中看到大量的goto语句:) (8认同)

Run*_* FS 5

如果保持在1(第一种情况),你会接到一个EndAwait没有电话的电话BeginAwait.如果它保持在2(第二种情况),你只能在另一个等待者身上获得相同的结果.

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

但是,假设BeginAwaiter实际上不会在第一个之后的任何调用上启动操作,并且在这些情况下它将返回false.开始当然是不可接受的,因为它可能有副作用或只是给出不同的结果.它还假设EndAwaiter总是返回相同的值,无论它被调用了多少次,并且当BeginAwait返回false时可以调用它(根据上述假设)

这似乎是对竞争条件的警惕如果我们内联那些在状态= 0之后由另一个线程调用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();
Run Code Online (Sandbox Code Playgroud)

如果上述假设是正确的,那么就会完成一些不必要的工作,例如获取锯齿并将相同的值重新分配给<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();
Run Code Online (Sandbox Code Playgroud)

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

  • 我建议您在http://connect.microsoft.com填写[潜在] BUG报告 (2认同)