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:
- The
Result
call waits on the task that has been returned fromTask.Run
and blocks the current thread 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).- The task that wrapped this
AsyncFunc
(AsyncFunc
is the task in step 2) is ready to return, but the wrappedTask
is created in the UI thread hence it has theSyncContext
. - The current
SyncContext
has only one thread and it is blocked due to the call toResult
- the wrapped task cannot be marked as completed and set the result
- deadlock
CodePudding user response:
- 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 SpinWait
ing 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 wrappedTask
is created in the UI thread hence it has theSyncContext
.
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.