1. ホーム
  2. c#

[解決済み】C# 5 非同期 CTP:生成されたコードで EndAwait 呼び出しの前に内部の "state" が 0 に設定されるのはなぜですか?

2022-04-14 17:36:57

質問

昨日、C#の新機能である "async"について、特に生成されたコードがどのように見えるかを掘り下げて講演していたのですが、その際に the GetAwaiter() / BeginAwait() / EndAwait() を呼び出します。

C#コンパイラが生成するステートマシンを詳しく見てみたのですが、理解できない点が2つありました。

  • なぜ生成されたクラスには Dispose() メソッドと $__disposing という変数がありますが、これらは一度も使われていないようです (そして、このクラスは IDisposable ).
  • なぜ、内部の state を呼び出す前に0に設定されます。 EndAwait() 通常、0は「最初のエントリーポイントです」という意味になります。

最初の点は、asyncメソッドの中でもっと面白いことをすれば答えが出るのではないかと思いますが、もし誰かがもっと詳しい情報を持っていれば、それを教えていただけるとうれしいです。しかし、この質問は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() ではありません。 が呼び出された場合、状態の変化は関係ありません。

これは単にコンパイラがイテレータブロック生成コードを非同期用に再利用したことによる副作用で、もっと明白な説明があるのではないでしょうか?

重要な免責事項

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

解決方法は?

さて、ようやく本当の答えが出ました。VBチームのLucian Wischikが、本当に理由があることを確認した後、私は自分自身でそれを解決したのです。彼に感謝します。 ブログ (上の アーカイブドットオルグ )は、ロックです。

ここで値0が特別なのは、それが ではない の直前にいる可能性のある有効な状態です。 await を使用することで、通常の場合 特に、ステートマシンが他の場所でテストすることになるような状態ではないのです。正でない値を使っても同じように動作すると思います。 論理的に は通常、終了を意味するため、正しくありません。今のところ、状態0に余分な意味を与えていると主張することもできますが、結局のところ、それはあまり重要ではありません。この質問のポイントは、なぜ状態が設定されるのかを知ることです。

この値は、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 は任意の待ち受けにできます。タスクかもしれないし、他の何かかもしれません。私のテストでは、これは常に 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 は例外を投げます。キャッチブロックを通過して、インクリメントする i を使用して、再びループを回します。 i はまだ3より小さい)。の前と同じ状態です。 GetResult を呼び出しますが、その中に入ると try ブロックは Try"を表示し、次のように呼び出します。 GetAwaiter また、stateが1でない場合のみ、この処理を行います。 state = 0 を指定すると、既存のアウェイターを使用して Console.WriteLine を呼び出します。

しかし、これは、チームが考えなければならないことの一例です。私がこの実装の責任者でなくてよかったです :)