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用に作成されたスレッドで続行されます。
とにかく、待機中のコードを続行せずにタスクの結果を設定する方法はありますか?
アプリケーションはコンソールアプリケーションです。
TaskCompletionSource.SetResult();を発見しました。戻る前にタスクを待機しているコードを呼び出します。私の場合、デッドロックが発生します。
はい、私は ブログ投稿 これを文書化しています(MSDNには文書化されていません)。デッドロックは次の2つの理由で発生します。
async
とブロックコードが混在している(つまり、async
メソッドがWait
を呼び出している)。TaskContinuationOptions.ExecuteSynchronously
_を使用してスケジュールされます。最も簡単な解決策から始めることをお勧めします:最初のものを削除する(1)つまり、async
とWait
の呼び出しを混在させないでください。
_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 ;を見てください。便利だと思うかもしれません。
アプリはコンソールアプリであるため、デフォルトの 同期コンテキスト で実行されます。ここで、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.ManagedThreadId
をTask.Result
内に格納し、await
の後に現在のスレッドのIDと比較することで、これをさらに改善できます。まだ同じスレッドを使用している場合は、await Task.Yield()
を実行します。
SendAwaitResponse
は実際のコードを簡略化したものであることは理解していますが、内部では完全に同期しています(質問で示した方法)。スレッドの切り替えが予想されるのはなぜですか。
とにかく、あなたはおそらくあなたが現在どのスレッドにいるかについての仮定をしないようにあなたのロジックを再設計するべきです。 await
とTask.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))
を実行することと大差ありません。私が考えることができる唯一の利点は、スレッドを切り替えるタイミングを明示的に制御できることです。このようにして、可能な限り同じスレッドにとどまることができます(たとえば、task2
、task3
、task4
の場合でも、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 から確認することをお勧めします。うまくいけば、スティーブンが飛び込んでより良い答えを提供します。
「私の想定では、2番目のSendAwaitResponseはThreadPoolスレッドで実行されますが、ReceiverRun用に作成されたスレッドで続行されます。」
それは、SendAwaitResponse内で何をするかに完全に依存します。非同期性と並行性 同じではない 。
チェックアウト: C#5 Async/Await-* concurrent *?
パーティーには少し遅れますが、これが付加価値だと思う私の解決策です。
私もこれに苦労しており、待機中のメソッドで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;
}
}