Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling TaskCompletionSource.SetResult in a non blocking manner

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.

like image 630
hultqvist Avatar asked Oct 20 '13 19:10

hultqvist


Video Answer


1 Answers

I've discovered that TaskCompletionSource.SetResult(); invokes the code awaiting the task before returning. In my case that result in a deadlock.

Yes, I have a blog post documenting this (AFAIK it's not documented on MSDN). The deadlock happens because of two things:

  1. There's a mixture of async and blocking code (i.e., an async method is calling Wait).
  2. Task continuations are scheduled using TaskContinuationOptions.ExecuteSynchronously.

I recommend starting with the simplest possible solution: removing the first thing (1). I.e., don't mix async and Wait calls:

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

Instead, use await consistently:

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

If you need to, you can Wait at an alternative point further up the call stack (not in an async 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 a Task.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 my AsyncContext type) and specify ConfigureAwait(false), which will cause the continuation to ignore the ExecuteSynchronously 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.

like image 95
Stephen Cleary Avatar answered Oct 08 '22 08:10

Stephen Cleary