web-dev-qa-db-ja.com

非ブロッキング方式でTaskCompletionSource.SetResultを呼び出す

TaskCompletionSource.SetResult();が戻る前にタスクを待機しているコードを呼び出すことを発見しました。私の場合、デッドロックが発生します。

これは、通常のThreadで開始される簡易バージョンです。

void ReceiverRun()
    while (true)
    {
        var msg = ReadNextMessage();
        TaskCompletionSource<Response> task = requests[msg.RequestID];

        if(msg.Error == null)
            task.SetResult(msg);
        else
            task.SetException(new Exception(msg.Error));
    }
}

コードの「非同期」部分は次のようになります。

await SendAwaitResponse("first message");
SendAwaitResponse("second message").Wait();

待機は実際には非非同期呼び出し内にネストされています。

SendAwaitResponse(簡略化)

public static Task<Response> SendAwaitResponse(string msg)
{
    var t = new TaskCompletionSource<Response>();
    requests.Add(GetID(msg), t);
    stream.Write(msg);
    return t.Task;
}

私の想定では、2番目のSendAwaitResponseはThreadPoolスレッドで実行されますが、ReceiverRun用に作成されたスレッドで続行されます。

とにかく、待機中のコードを続行せずにタスクの結果を設定する方法はありますか?

アプリケーションはコンソールアプリケーションです。

27
hultqvist

TaskCompletionSource.SetResult();を発見しました。戻る前にタスクを待機しているコードを呼び出します。私の場合、デッドロックが発生します。

はい、私は ブログ投稿 これを文書化しています(MSDNには文書化されていません)。デッドロックは次の2つの理由で発生します。

  1. asyncとブロックコードが混在している(つまり、asyncメソッドがWaitを呼び出している)。
  2. タスクの継続は、_TaskContinuationOptions.ExecuteSynchronously_を使用してスケジュールされます。

最も簡単な解決策から始めることをお勧めします:最初のものを削除する(1)つまり、asyncWaitの呼び出しを混在させないでください。

_await SendAwaitResponse("first message");
SendAwaitResponse("second message").Wait();
_

代わりに、awaitを一貫して使用します。

_await SendAwaitResponse("first message");
await SendAwaitResponse("second message");
_

必要に応じて、代替ポイントでWaitを使用して、コールスタックをさらに上に移動できます(notin async方法)。

それが私の最も推奨されるソリューションです。ただし、2番目のもの(2)を削除したい場合は、いくつかのトリックを実行できます。SetResultを_Task.Run_でラップして、強制的に別のスレッド(my- AsyncExライブラリ には_*WithBackgroundContinuations_拡張メソッドがあり、これを正確に実行します)、またはスレッドに実際のコンテキスト(my AsyncContext type など)を指定して、ConfigureAwait(false)、これは 継続によりExecuteSynchronouslyフラグが無視されるため になります。

しかし、これらのソリューションは、asyncとブロックコードを分離するだけではなく、はるかに複雑です。

補足として、 TPL Dataflow ;を見てください。便利だと思うかもしれません。

28
Stephen Cleary

アプリはコンソールアプリであるため、デフォルトの 同期コンテキスト で実行されます。ここで、await継続コールバックは、待機中のタスクが完了した同じスレッドで呼び出されます。 await SendAwaitResponseの後にスレッドを切り替えたい場合は、await Task.Yield()を使用して切り替えることができます。

await SendAwaitResponse("first message");
await Task.Yield(); 
// will be continued on a pool thread
// ...
SendAwaitResponse("second message").Wait(); // so no deadlock

Thread.CurrentThread.ManagedThreadIdTask.Result内に格納し、awaitの後に現在のスレッドのIDと比較することで、これをさらに改善できます。まだ同じスレッドを使用している場合は、await Task.Yield()を実行します。

SendAwaitResponseは実際のコードを簡略化したものであることは理解していますが、内部では完全に同期しています(質問で示した方法)。スレッドの切り替えが予想されるのはなぜですか。

とにかく、あなたはおそらくあなたが現在どのスレッドにいるかについての仮定をしないようにあなたのロジックを再設計するべきです。 awaitTask.Wait()を混在させないで、すべてのコードを非同期にします。通常、トップレベルのどこかにWait()を1つだけ使用することができます(例:Main内)。

[EDITED] ReceiverRunからtask.SetResult(msg)を呼び出すと、制御フローが実際にawait上のtaskに転送されます-デフォルトの同期コンテキストの動作のため、スレッドを切り替える必要はありません。したがって、実際のメッセージ処理を行うコードは、ReceiverRunスレッドを引き継ぎます。最終的に、SendAwaitResponse("second message").Wait()が同じスレッドで呼び出され、デッドロックが発生します。

以下は、サンプルをモデルにしたコンソールアプリのコードです。 ProcessAsync内でawait Task.Yield()を使用して、別のスレッドで継続をスケジュールするため、制御フローはReceiverRunに戻り、デッドロックは発生しません。

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {
        class Worker
        {
            public struct Response
            {
                public string message;
                public int threadId;
            }

            CancellationToken _token;
            readonly ConcurrentQueue<string> _messages = new ConcurrentQueue<string>();
            readonly ConcurrentDictionary<string, TaskCompletionSource<Response>> _requests = new ConcurrentDictionary<string, TaskCompletionSource<Response>>();

            public Worker(CancellationToken token)
            {
                _token = token;
            }

            string ReadNextMessage()
            {
                // using Thread.Sleep(100) for test purposes here,
                // should be using ManualResetEvent (or similar synchronization primitive),
                // depending on how messages arrive
                string message;
                while (!_messages.TryDequeue(out message))
                {
                    Thread.Sleep(100);
                    _token.ThrowIfCancellationRequested();
                }
                return message;
            }

            public void ReceiverRun()
            {
                LogThread("Enter ReceiverRun");
                while (true)
                {
                    var msg = ReadNextMessage();
                    LogThread("ReadNextMessage: " + msg);
                    var tcs = _requests[msg];
                    tcs.SetResult(new Response { message = msg, threadId = Thread.CurrentThread.ManagedThreadId });
                    _token.ThrowIfCancellationRequested(); // this is how we terminate the loop
                }
            }

            Task<Response> SendAwaitResponse(string msg)
            {
                LogThread("SendAwaitResponse: " + msg);
                var tcs = new TaskCompletionSource<Response>();
                _requests.TryAdd(msg, tcs);
                _messages.Enqueue(msg);
                return tcs.Task;
            }

            public async Task ProcessAsync()
            {
                LogThread("Enter Worker.ProcessAsync");

                var task1 = SendAwaitResponse("first message");
                await task1;
                LogThread("result1: " + task1.Result.message);
                // avoid deadlock for task2.Wait() with Task.Yield()
                // comment this out and task2.Wait() will dead-lock
                if (task1.Result.threadId == Thread.CurrentThread.ManagedThreadId)
                    await Task.Yield();

                var task2 = SendAwaitResponse("second message");
                task2.Wait();
                LogThread("result2: " + task2.Result.message);

                var task3 = SendAwaitResponse("third message");
                // still on the same thread as with result 2, no deadlock for task3.Wait()
                task3.Wait();
                LogThread("result3: " + task3.Result.message);

                var task4 = SendAwaitResponse("fourth message");
                await task4;
                LogThread("result4: " + task4.Result.message);
                // avoid deadlock for task5.Wait() with Task.Yield()
                // comment this out and task5.Wait() will dead-lock
                if (task4.Result.threadId == Thread.CurrentThread.ManagedThreadId)
                    await Task.Yield();

                var task5 = SendAwaitResponse("fifth message");
                task5.Wait();
                LogThread("result5: " + task5.Result.message);

                LogThread("Leave Worker.ProcessAsync");
            }

            public static void LogThread(string message)
            {
                Console.WriteLine("{0}, thread: {1}", message, Thread.CurrentThread.ManagedThreadId);
            }
        }

        static void Main(string[] args)
        {
            Worker.LogThread("Enter Main");
            var cts = new CancellationTokenSource(5000); // cancel after 5s
            var worker = new Worker(cts.Token);
            Task receiver = Task.Run(() => worker.ReceiverRun());
            Task main = worker.ProcessAsync();
            try
            {
                Task.WaitAll(main, receiver);
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception: " + e.Message);
            }
            Worker.LogThread("Leave Main");
            Console.ReadLine();
        }
    }
}

これは、ReceiverRun内でTask.Run(() => task.SetResult(msg))を実行することと大差ありません。私が考えることができる唯一の利点は、スレッドを切り替えるタイミングを明示的に制御できることです。このようにして、可能な限り同じスレッドにとどまることができます(たとえば、task2task3task4の場合でも、task5.Wait()でのデッドロックを回避するには、task4の後に別のスレッドスイッチが必要です)。

どちらのソリューションでも、最終的にはスレッドプールが大きくなりますが、これはパフォーマンスとスケーラビリティの点で悪いことです。

task.Wait()await taskで上記のコードのProcessAsync内のすべての場所に置き換えた場合、await Task.Yieldを使用する必要はなく、デッドロックは発生しません。ただし、await内の最初のawait task1の後のProcessAsync呼び出しのチェーン全体は、実際にはReceiverRunスレッドで実行されます。他のWait()スタイルの呼び出しでこのスレッドをブロックせず、メッセージを処理しているときにCPUに依存する多くの作業を行わない限り、このアプローチは問題なく動作する可能性があります(非同期IOにバインドされます) await- styleの呼び出しは引き続き問題ありませんが、実際には暗黙的なスレッド切り替えがトリガーされる場合があります)。

とはいえ、メッセージを処理するには、シリアル化同期コンテキストがインストールされた別のスレッドが必要になると思います(WindowsFormsSynchronizationContextと同様)。ここで、awaitsを含む非同期コードを実行する必要があります。そのスレッドでTask.Waitを使用しないようにする必要があります。また、個々のメッセージ処理がCPUに依存する多くの作業を行う場合、そのような作業にはTask.Runを使用する必要があります。非同期IOバインドの呼び出しの場合、同じスレッドにとどまることができます。

ActionDispatcher/ActionDispatcherSynchronizationContextを非同期メッセージ処理ロジックの @ StephenCleary 's Nito Asynchronous Library から確認することをお勧めします。うまくいけば、スティーブンが飛び込んでより良い答えを提供します。

5
noseratio

「私の想定では、2番目のSendAwaitResponseはThreadPoolスレッドで実行されますが、ReceiverRun用に作成されたスレッドで続行されます。」

それは、SendAwaitResponse内で何をするかに完全に依存します。非同期性と並行性 同じではない

チェックアウト: C#5 Async/Await-* concurrent *?

0
Slugart

パーティーには少し遅れますが、これが付加価値だと思う私の解決策です。

私もこれに苦労しており、待機中のメソッドでSynchronizationContextをキャプチャすることで解決しました。

それは次のようになります:

// just a default sync context
private readonly SynchronizationContext _defaultContext = new SynchronizationContext();

void ReceiverRun()
{
    while (true)    // <-- i would replace this with a cancellation token
    {
        var msg = ReadNextMessage();
        TaskWithContext<TResult> task = requests[msg.RequestID];

        // if it wasn't a winforms/wpf thread, it would be null
        // we choose our default context (threadpool)
        var context = task.Context ?? _defaultContext;

        // execute it on the context which was captured where it was added. So it won't get completed on this thread.
        context.Post(state =>
        {
            if (msg.Error == null)
                task.TaskCompletionSource.SetResult(msg);
            else
                task.TaskCompletionSource.SetException(new Exception(msg.Error));
        });
    }
}

public static Task<Response> SendAwaitResponse(string msg)
{
    // The key is here! Save the current synchronization context.
    var t = new TaskWithContext<Response>(SynchronizationContext.Current); 

    requests.Add(GetID(msg), t);
    stream.Write(msg);
    return t.TaskCompletionSource.Task;
}

// class to hold a task and context
public class TaskWithContext<TResult>
{
    public SynchronizationContext Context { get; }

    public TaskCompletionSource<TResult> TaskCompletionSource { get; } = new TaskCompletionSource<Response>();

    public TaskWithContext(SynchronizationContext context)
    {
        Context = context;
    }
}
0