web-dev-qa-db-ja.com

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

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

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

GetWorkAsync()は待機が可能なTaskを返すと想定されています。これにより、継続が実行されたときにスレッド切り替えが発生する場合と発生しない場合があります。

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

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

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

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

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

10
aoven

Try 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の説明 "It's not magic" を参照して、どのように作業できるかを説明してくださいasync/await 1つのスレッドのみ。

6
Mason Wheeler

まず、Servyは同様の質問への回答にいくつかのコードを記述しており、この回答に基づいています。

https://stackoverflow.com/questions/22049339/how-to-create-a-cancellable-task-loop

Servyの回答には、asyncおよびawaitキーワードを明示的に使用せずに、TPL構成を使用した同様のContinueWith()ループが含まれています。質問に答えるには、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()を呼び出して「内部タスク」(つまり、actualresult of 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()の状態で更新されているため、GetWorkAsync()が戻るときにcontinuationラムダが呼び出されます。

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
Ben Cottrell