我发现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();
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;
}
我的假设是第二个SendAwaitResponse将在ThreadPool线程中执行,但它会在为ReceiverRun创建的线程中继续。
无论如何设置任务的结果而不继续等待代码?
该应用程序是控制台应用程序。
答案 0 :(得分:26)
我发现了TaskCompletionSource.SetResult();在返回之前调用等待任务的代码。在我的情况下导致死锁。
是的,我有blog post记录了这一点(AFAIK没有记录在MSDN上)。死锁发生的原因有两个:
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
{{1>}方法中而不是)。
这是我最推荐的解决方案。但是,如果你想尝试删除第二个东西(2),你可以做一些技巧:将async
包裹在SetResult
中以强制它进入一个单独的线程(my {{3有Task.Run
扩展方法可以完成此操作),或者为您的线程提供实际的上下文(例如我的AsyncEx library)并指定*WithBackgroundContinuations
,这将AsyncContext
type。< / p>
但这些解决方案比分离ConfigureAwait(false)
和阻止代码要复杂得多。
作为旁注,请看cause the continuation to ignore the ExecuteSynchronously
flag;听起来你可能觉得它很有用。
答案 1 :(得分:5)
由于您的应用是一个控制台应用,它运行在默认的synchronization context上,其中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()
(例如,在Main
内)。
[已编辑] 从task.SetResult(msg)
拨打ReceiverRun
实际上将控制流转移到await
上task
的位置 - 没有线程切换,因为默认的同步上下文的行为。因此,执行实际消息处理的代码将接管ReceiverRun
线程。最终,在同一个线程上调用SendAwaitResponse("second message").Wait()
,导致死锁。
以下是以您的示例为模型的控制台应用代码。它使用await Task.Yield()
内的ProcessAsync
来计划单独线程上的延续,因此控制流返回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();
}
}
}
这与在Task.Run(() => task.SetResult(msg))
内执行ReceiverRun
没有多大区别。我能想到的唯一优势是你可以明确控制何时切换线程。这样,您可以尽可能长时间保持在同一个线程上(例如,对于task2
,task3
,task4
,但在task4
之后仍需要另一个线程切换避免task5.Wait()
)出现僵局。
这两种解决方案最终都会使线程池增长,这在性能和可伸缩性方面都很糟糕。
现在,如果我们在上述代码中task.Wait()
内的await task
替换ProcessAsync
await Task.Yield
,我们就不必使用await
和仍然没有死锁。但是,在await task1
内ProcessAsync
ReceiverRun
内的Wait()
调用之后的整个await
调用实际上将在WindowsFormsSynchronizationContext
线程上执行。只要我们不用其他awaits
样式的调用来阻止这个线程,并且在我们处理消息时不做大量的CPU绑定工作,这种方法可能正常工作(异步IO绑定{ {1}} - 样式调用仍然应该没问题,它们实际上可能会触发隐式线程切换)。
那就是说,我认为你需要一个单独的线程,其上安装了序列化同步上下文来处理消息(类似于Task.Wait
)。这就是运行包含Task.Run
的异步代码的地方。您仍然需要避免在该线程上使用ActionDispatcher
。如果单个消息处理需要大量CPU限制工作,则应使用ActionDispatcherSynchronizationContext
进行此类工作。对于异步IO绑定调用,您可以保持在同一个线程上。
您可能希望查看来自@StephenCleary的{{1}} / {{1}} Nito Asynchronous Library用于异步消息处理逻辑。希望斯蒂芬跳进来并提供更好的答案。
答案 2 :(得分:0)
“我的假设是第二个SendAwaitResponse将在ThreadPool线程中执行,但它会在为ReceiverRun创建的线程中继续。”
这完全取决于您在SendAwaitResponse中执行的操作。异步和并发are not the same thing。
答案 3 :(得分:0)
聚会晚了一点,但这是我的解决方案,我认为这是附加值。
我也一直在为此苦苦挣扎,我通过在等待的方法上捕获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;
}
}