昨日、新しいC#の「非同期」機能について、特に生成されたコードがどのように見えるか、およびthe GetAwaiter()
/BeginAwait()
/EndAwait()
について詳しく説明しました呼び出します。
C#コンパイラによって生成されたステートマシンを詳細に調べましたが、理解できない2つの側面がありました。
Dispose()
メソッドと_$__disposing
_変数が含まれており、これらは使用されていないように見える(そしてクラスはIDisposable
を実装しない)理由。EndAwait()
を呼び出す前に内部state
変数が0に設定されるのか、通常0が「これが初期エントリポイント」を意味するように見える場合。非同期メソッド内でもっと面白いことをすることで最初のポイントに答えられると思いますが、さらに情報があれば、それを聞いてうれしいです。ただし、この質問は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#コンパイラまたはそのようなものの欠陥であると主張しようとするものではありません。私が見逃したこの微妙な理由があるかどうかを解決しようとしています:)
1に保持されている場合(最初の場合)、EndAwait
を呼び出さずにBeginAwait
を呼び出します。 2(2番目のケース)に保たれている場合、他のウェイターで同じ結果が得られます。
BeginAwaitを呼び出すと、既に開始されている場合はfalseを返し(私の側からの推測)、EndAwaitで返される元の値を保持していると推測しています。その場合は正しく動作しますが、-1に設定すると初期化されていないthis.<1>t__$await1
最初の場合。
ただし、これは、BeginAwaiterが実際に最初の呼び出し以降の呼び出しでアクションを開始せず、それらの場合にfalseを返すことを前提としています。副作用があったり、単に異なる結果が得られたりする可能性があるため、開始はもちろん受け入れられません。また、EndAwaiterは、何度呼び出されても常に同じ値を返し、BeginAwaitがfalseを返したときに呼び出すことができると仮定しています(上記の仮定に従って)
競合状態に対するガードのように思えます。state= 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();
上記の仮定が正しければ、sawiaterを取得し、同じ値を<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に設定されている場合、ステートマシンは、最初のアクションの値がすでにtrueになっていると想定し、(潜在的に)未割り当ての変数を使用して結果を計算します
スタックされた/ネストされた非同期呼び出しと何か関係がありますか?..
すなわち:
async Task m1()
{
await m2;
}
async Task m2()
{
await m3();
}
async Task m3()
{
Thread.Sleep(10000);
}
この状況でmovenextデリゲートは複数回呼び出されますか?
本当にパント?
実際の状態の説明:
可能な状態:
この実装は、(待機中に)どこからでもMoveNextへの別の呼び出しが発生した場合、状態チェーン全体を最初から再評価することを保証したいだけです平均的な結果を再評価するためにすでに古くなっていますか?