I've discovered that TaskCompletionSource.SetResult();
invokes the code awaiting the task before returning. In my case that result in a deadlock.
This is a simplified version that is started in an ordinary 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));
}
}
The "async" part of the code looks something like this.
await SendAwaitResponse("first message");
SendAwaitResponse("second message").Wait();
The Wait is actually nested inside non-async calls.
The SendAwaitResponse(simplified)
public static Task<Response> SendAwaitResponse(string msg)
{
var t = new TaskCompletionSource<Response>();
requests.Add(GetID(msg), t);
stream.Write(msg);
return t.Task;
}
My assumption was that the second SendAwaitResponse would execute in a ThreadPool thread but it continues in the thread created for ReceiverRun.
Is there anyway to set the result of a task without continuing its awaited code?
The application is a console application.
A little late to the party, but here's my solution which i think is added value.
I've been struggling with this also, i've solved it by capturing the SynchronizationContext on the method that is awaited.
It would look something like:
"My assumption was that the second SendAwaitResponse would execute in a ThreadPool thread but it continues in the thread created for ReceiverRun."
It depends entirely on what you do within SendAwaitResponse. Asynchrony and concurrency are not the same thing.
Check out: C# 5 Async/Await - is it *concurrent*?
Yes, I have a blog post documenting this (AFAIK it's not documented on MSDN). The deadlock happens because of two things:
async
and blocking code (i.e., anasync
method is callingWait
).TaskContinuationOptions.ExecuteSynchronously
.I recommend starting with the simplest possible solution: removing the first thing (1). I.e., don't mix
async
andWait
calls:Instead, use
await
consistently:If you need to, you can
Wait
at an alternative point further up the call stack (not in anasync
method).That's my most-recommended solution. However, if you want to try removing the second thing (2), you can do a couple of tricks: either wrap the
SetResult
in aTask.Run
to force it onto a separate thread (my AsyncEx library has*WithBackgroundContinuations
extension methods that do exactly this), or give your thread an actual context (such as myAsyncContext
type) and specifyConfigureAwait(false)
, which will cause the continuation to ignore theExecuteSynchronously
flag.But those solutions are much more complex than just separating the
async
and blocking code.As a side note, take a look at TPL Dataflow; it sounds like you may find it useful.
As your app is a console app, it runs on the default synchronization context, where the
await
continuation callback will be called on the same thread the awaiting task has become completed on. If you want to switch threads afterawait SendAwaitResponse
, you can do so withawait Task.Yield()
:You could further improve this by storing
Thread.CurrentThread.ManagedThreadId
insideTask.Result
and comparing it to the current thread's id after theawait
. If you're still on the same thread, doawait Task.Yield()
.While I understand that
SendAwaitResponse
is a simplified version of your actual code, it's still completely synchronous inside (the way you showed it in your question). Why would you expect any thread switch in there?Anyway, you probably should redesign your logic the way it doesn't make assumptions about what thread you are currently on. Avoid mixing
await
andTask.Wait()
and make all of your code asynchronous. Usually, it's possible to stick with just oneWait()
somewhere on the top level (e.g. insideMain
).[EDITED] Calling
task.SetResult(msg)
fromReceiverRun
actually transfers the control flow to the point where youawait
on thetask
- without a thread switch, because of the default synchronization context's behavior. So, your code which does the actual message processing is taking over theReceiverRun
thread. Eventually,SendAwaitResponse("second message").Wait()
is called on the same thread, causing the deadlock.Below is a console app code, modeled after your sample. It uses
await Task.Yield()
insideProcessAsync
to schedule the continuation on a separate thread, so the control flow returns toReceiverRun
and there's no deadlock.This is not much different from doing
Task.Run(() => task.SetResult(msg))
insideReceiverRun
. The only advantage I can think of is that you have an explicit control over when to switch threads. This way, you can stay on the same thread for as long as possible (e.g., fortask2
,task3
,task4
, but you still need another thread switch aftertask4
to avoid a deadlock ontask5.Wait()
).Both solutions would eventually make the thread pool grow, which is bad in terms of performance and scalability.
Now, if we replace
task.Wait()
withawait task
everywhere insideProcessAsync
in the above code, we will not have to useawait Task.Yield
and there still will be no deadlocks. However, the whole chain ofawait
calls after the 1stawait task1
insideProcessAsync
will actually be executed on theReceiverRun
thread. As long as we don't block this thread with otherWait()
-style calls and don't do a lot of CPU-bound work as we're processing messages, this approach might work OK (asynchronous IO-boundawait
-style calls still should be OK, and they may actually trigger an implicit thread switch).That said, I think you'd need a separate thread with a serializing synchronization context installed on it for processing messages (similar to
WindowsFormsSynchronizationContext
). That's where your asynchronous code containingawaits
should run. You'd still need to avoid usingTask.Wait
on that thread. And if an individual message processing takes a lot of CPU-bound work, you should useTask.Run
for such work. For async IO-bound calls, you could stay on the same thread.You may want to look at
ActionDispatcher
/ActionDispatcherSynchronizationContext
from @StephenCleary's Nito Asynchronous Library for your asynchronous message processing logic. Hopefully, Stephen jumps in and provides a better answer.