Home > Software design >  How Task.Run avoids deadlocks in async code?
How Task.Run avoids deadlocks in async code?

Time:12-31

I've read from async guidelines that if we HAVE to block the async code on a UI thread(or single threaded SyncContext) then we must block it by using

Task.Run(()=>AsyncFunc()).Result

inorder to avoid potential deadlocks.

quote from the link:

public string DoOperationBlocking()
{
    // Bad - Blocking the thread that enters.
    // DoAsyncOperation will be scheduled on the default task scheduler, **and remove the risk of deadlocking.**
    // In the case of an exception, this method will throw an AggregateException wrapping the original exception.
    return Task.Run(() => DoAsyncOperation()).Result;
}

note this part:

...and remove the risk of deadlocking.

But I'm a bit confused on how this would prevent such thing. Based on my understanding, Task.Run passes the paramater as a Task to ThreadPool which does NOT have a SyncContext hence not blocking our current thread in the SyncContext. But let's see what actually happens based on my understanding:

  1. The Result call waits on the task that has been returned from Task.Run and blocks the current thread
  2. AsyncFunc has now done its job and it is ready to return. It actually can since it is not bound to return to any specific thread (this task does not have the SyncContext).
  3. The task that wrapped this AsyncFunc (AsyncFunc is the task in step 2) is ready to return, but the wrapped Task is created in the UI thread hence it has the SyncContext.
  4. The current SyncContext has only one thread and it is blocked due to the call to Result
  5. the wrapped task cannot be marked as completed and set the result
  6. deadlock

CodePudding user response:

  1. The wrapped task cannot be marked as completed and set the result

AFAIK your understanding here is incorrect. The wrapped task (AsyncFunc) can mark the wrapper one as completed from another thread (and nothing prevents the wrapped one from completion because it executes on the default scheduler on thread pool, see the implementation and does not use the SynchronizationContext).

If you check Task.Run<TResult>(Func<Task<TResult>?> function) implementation you will see that it uses internally UnwrapPromise which internally adds callback for the wrapped task if it is not already completed - outerTask.AddCompletionAction(this); (outer is the wrapped async task in this context), since the default scheduler was used the continuation does not use the SynchronizationContext, so there is no deadlock.

Also Task.Result implementation can shed some light. For not finished tasks it calls GetResultCore which does the blocking which combines SpinWaiting and ManualResetEventSlim which can be signaled from another thread.

CodePudding user response:

The task that wrapped this AsyncFunc (AsyncFunc is the task in step 2) is ready to return, but the wrapped Task is created in the UI thread hence it has the SyncContext.

It is true that the Task is created on the UI thread, but it is not generated by an async method. The Task.Run method is not implemented with the async keyword. The SynchronizationContext plays a role in the await points (it is captured by default). There are no await points in a non-async method.

Take a look at this asynchronous method for example:

Task MyYield()
{
    TaskCompletionSource tcs = new();
    ThreadPool.QueueUserWorkItem(_ => tcs.SetResult());
    return tcs.Task;
}

Notice the absence of the async keyword. You can call MyYield().Wait(); on the UI thread as much as you want. No deadlock will happen¹. The completion of the TaskCompletionSource does not depend on the UI thread. You can imagine that the Task.Run is implemented in a similar way.

¹ Unless the ThreadPool has reached its maximum size, and all the available threads are permanently blocked.

  • Related