スレッドがwhileループ内でタスクを待機すると、正確にはどうなりますか?


10

しばらくC#の非同期/待機パターンを処理した後、次のコードで何が発生するかを説明する方法が本当にわからないことに突然気づきました。

async void MyThread()
{
    while (!_quit)
    {
        await GetWorkAsync();
    }
}

GetWorkAsync()Task継続が実行されたときにスレッド切り替えを引き起こす場合とそうでない場合があるawaitableを返すと想定されます。

待機がループ内になかったとしても、混乱することはありません。私は当然、メソッドの残りの部分(つまり、継続)が別のスレッドで実行される可能性があることを期待しています。

ただし、ループ内では、「メソッドの残りの部分」の概念が少し曖昧になります。

スレッドが継続で切り替えられた場合と切り替えられなかった場合の「ループの残りの部分」はどうなりますか?ループの次の反復はどのスレッドで実行されますか?

私の観察では、各反復が同じスレッド(元のスレッド)で開始され、継続が別のスレッドで実行されていることが(最終的に検証されていません)示されています。これは本当にあり得ますか?はいの場合、これは、GetWorkAsyncメソッドのスレッドセーフティに対して考慮される必要があるある程度の予期しない並列処理ですか?

更新:一部の人が示唆したように、私の質問は重複ではありません。while (!_quit) { ... }コードパターンは単に私の実際のコードを簡略化したものです。実際には、私のスレッドは、定期的に(デフォルトでは5秒おきに)作業項目の入力キューを処理する長期ループです。実際の終了条件チェックも、サンプルコードで提案されている単純なフィールドチェックではなく、イベントハンドルチェックです。



1
.NETで制御フローを実装および待機する方法についても参照してくださいこれがすべて一緒に配線されている方法に関するいくつかの素晴らしい情報については。
John Wu

@ジョンウー:私はまだそのSOスレッドを見ていません。興味深い情報がたくさんあります。ありがとう!
オーブン

回答:


6

実際に試すには、Roslyn試してみてください。awaitメソッドがvoid IAsyncStateMachine.MoveNext()、生成された非同期クラスに書き換えられます。

表示されるのは次のようなものです。

            if (this.state != 0)
                goto label_2;
            //set up the state machine here
            label_1:
            taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter);
            label_2:
            if (!OuterClass._quit)
            {
               taskAwaiter = GetWorkAsync().GetAwaiter();
               //state machine stuff here
            }
            goto label_1;

基本的に、どのスレッドにいるかは関係ありません。ループを同等のif / goto構造で置き換えることにより、状態マシンは適切に再開できます。

そうは言っても、非同期メソッドは必ずしも別のスレッドで実行されるとは限りません。Eric Lippertの「魔法ではない」の説明を参照してasync/await、1つのスレッドだけで作業する方法を説明してください。


2
コンパイラが非同期コードで行う書き換えの範囲を過小評価しているようです。本質的に、書き換え後の「ループ」はありません。それは私にとって欠けていた部分でした。「Roslynをお試しください」リンクもありがとうございます。
aoven

GOTOは元のループ構造です。それを忘れないでください。

2

まず、Servyは同様の質問への回答にいくつかのコードを記述しました。

/programming/22049339/how-to-create-a-cancellable-task-loop

Servyの回答にはContinueWith()asyncおよびawaitキーワードを明示的に使用せずにTPL構造を使用した同様のループが含まれています。だからあなたの質問に答えるために、ループが展開されたときにあなたのコードがどのように見えるかを考えてくださいContinueWith()

    private static Task GetWorkWhileNotQuit()
    {
        var tcs = new TaskCompletionSource<bool>();

        Task previous = Task.FromResult(_quit);
        Action<Task> continuation = null;
        continuation = t =>
        {
            if (!_quit)
            {
                previous = previous.ContinueWith(_ => GetWorkAsync())
                    .Unwrap()
                    .ContinueWith(_ => previous.ContinueWith(continuation));
            }
            else
            {
                tcs.SetResult(_quit);
            }
        };
        previous.ContinueWith(continuation);
        return tcs.Task;
    }

これはあなたの頭を包み込むのに少し時間がかかりますが、要約すると:

  • continuation「現在の反復」のクロージャーを表す
  • previous「前の反復」Taskの状態を含むことを表します(つまり、「反復」がいつ終了し、次の反復の開始に使用されるかを認識します。)
  • がをGetWorkAsync()返すと仮定するとTask、つまり、「内部タスク」(つまりの実際の結果)を取得ContinueWith(_ => GetWorkAsync())するTask<Task>ための呼び出しが返さUnwrap()れます。GetWorkAsync()

そう:

  1. 最初は前の反復がないため、単に値が割り当てられますTask.FromResult(_quit) -その状態はとして始まりTask.Completed == trueます。
  2. continuation使用して初めて実行されますprevious.ContinueWith(continuation)
  3. の完了状態を反映するようにcontinuationクロージャが更新さpreviousれます_ => GetWorkAsync()
  4. ときに_ => GetWorkAsync()完了すると、それは「と続けて」_previous.ContinueWith(continuation)-すなわち呼び出しcontinuation、再びラムダを
    • 明らかにこの時点previousで、の状態で更新されている_ => GetWorkAsync()ため、戻りcontinuation時にラムダが呼び出されGetWorkAsync()ます。

continuationラムダはいつもの状態チェックし_quitた場合、そうし_quit == falseた後、それ以上の継続が存在しない、とTaskCompletionSourceの値に設定されます_quit、そして、すべてが完成します。

別のスレッドで実行されている継続に関する観察については、このブログの「タスクは(まだ)スレッドではなく、非同期は並列ではない」のでasync/ awaitキーワードがあなたのために行うことではありません。- https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

GetWorkAsync()スレッド化とスレッドの安全性に関して、メソッドを詳細に検討する価値があることをお勧めします。診断により、非同期/待機コードが繰り返された結果、別のスレッドで実行されていることが判明した場合は、そのメソッド内またはそのメソッドに関連する何かが原因で、新しいスレッドが別の場所に作成されている必要があります。(これが予想外の場合、おそらく.ConfigureAwaitどこかにありますか?)


2
私が示したコードは(非常に)単純化されています。GetWorkAsync()内には、さらに複数の待機があります。それらの一部はデータベースとネットワークにアクセスします。つまり、真のI / Oです。私が理解しているように、最初のスレッドは継続が実行される場所を管理する同期コンテキストを確立しないため、スレッドの切り替えはそのような待機の自然な結果です(必須ではありません)。したがって、スレッドプールスレッドで実行されます。私の推論は間違っていますか?
オーブン

@aoven良い点-私はさまざまな種類を考慮していませんでしたSynchronizationContext- .ContinueWith()継続をディスパッチするためにSynchronizationContextを使用するため、これは確かに重要です。awaitThreadPoolスレッドまたはASP.NETスレッドで呼び出された場合の動作を実際に説明します。それらの場合、継続は確かに別のスレッドにディスパッチされる可能性があります。一方、awaitWPF DispatcherやWinformsコンテキストなどのシングルスレッドコンテキストを呼び出すだけで、元のコンテキストで継続が発生します。スレッド
Ben Cottrell
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.