C# 5 非同期 CTP: EndAwait 呼び出しの前に生成されたコードで内部の「状態」が 0 に設定されるのはなぜですか? 質問する

C# 5 非同期 CTP: EndAwait 呼び出しの前に生成されたコードで内部の「状態」が 0 に設定されるのはなぜですか? 質問する

昨日、私は新しい C# の「async」機能について、特に生成されたコードがどのようになっているか、および // 呼び出しについて詳しく説明する講演を行いthe GetAwaiter()ましBeginAwait()EndAwait()

C# コンパイラによって生成されたステート マシンを詳しく調べたところ、理解できない点が 2 つありました。

  • 生成されたクラスに、決して使用されないDispose()メソッドと変数が含まれているのはなぜですか (クラスは を実装していません)。$__disposingIDisposable
  • 通常、0 は「これが最初のエントリ ポイントである」ことを意味するように見えるのに、stateを呼び出す前に内部変数が 0 に設定されるのはなぜですか。EndAwait()

最初のポイントは、非同期メソッド内でもっと興味深いことを実行することで解決できると思いますが、さらに詳しい情報をお持ちの方がいらっしゃれば、ぜひ教えてください。ただし、この質問は主に 2 番目のポイントに関するものです。

以下に非常にシンプルなサンプルコードを示します。

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)、非同期メソッドが再度開始され、私の知る限り、これは完全に不適切です... およびMoveNext() ない呼び出された場合、状態の変化は無関係です。

これは単に、より明白な説明があるかもしれない、コンパイラが async の反復子ブロック生成コードを再利用することによる副作用なのでしょうか?

重要な免責事項

明らかに、これは CTP コンパイラにすぎません。最終リリース前に、そしておそらく次の CTP リリース前にも、状況が変わることは十分予想されます。この質問は、これが C# コンパイラの欠陥であるとか、そのようなものであると主張しようとしているわけではありません。私は、私が見逃している微妙な理由があるかどうかを調べようとしているだけです :)

ベストアンサー1

ついに本当の答えが見つかりました。私は自分で何とか解決しましたが、VBチームのLucian Wischikが、本当にそれにはちゃんとした理由があることを確認した後でした。彼には大変感謝しています。そして、ぜひ訪問してください。彼のブログ(の上アーカイブ)、最高です。

ここでの0という値は、ない通常の場合、直前にいたかもしれない有効な状態await。特に、ステートマシンが他の場所でテストする可能性のある状態ではありません。正でない値を使用しても同様に機能すると思います。-1は、論理的に正しくありません。-1 は通常「終了」を意味します。現時点では状態 0 に追加の意味を与えていると主張することもできますが、結局のところ、それはあまり重要ではありません。この質問のポイントは、そもそも状態が設定されている理由を見つけることです。

この値は、awaitが例外で終了し、それがキャッチされた場合に関係します。同じawait文に再度戻ることもありますが、してはいけない「await から戻ろうとしている」という意味の状態である必要があります。そうしないと、あらゆる種類のコードがスキップされます。例を使用してこれを示すのが最も簡単です。現在 2 番目の 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

生成されたコードは次のとおりです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再度呼び出します...これは、状態が 1 でない場合にのみ実行されます。割り当てがない場合state = 0、既存の awaiter が使用され、Console.WriteLine呼び出しはスキップされます。

これはかなり複雑なコードですが、チームが考えなければならない事柄の種類を示すものです。これを実装する責任が私になくてよかったです :)

おすすめ記事