Separate synchronous and asynchronous code paths
See original GitHub issueMost of our public API has both synchronous and asynchronous versions of service methods. Due to the big amount of logic that is identical in both synchronous and asynchronous code paths, it is acceptable to have common non-public asynchronous methods that handle both code paths.
However, synchronous execution of asynchronous method may result in any of the following issues:
- Waiting for incomplete
ValueTask
. WhenValueTask
is created fromIValueTaskSource
, it is not guaranteed to return proper value (or any value at all) ifGetAwaiter().GetResult()
is called before task is completed. For example, implementation ofInteractiveBrowserCredential
has such issue:GetTokenImplAsync
will return incomplete task to theGetToken
while awaiting forAcquireTokenSilentAsync
. See docs for details. - Causing a deadlock on synchronization contexts. Any single-threaded
SynchronizationContext
(the most common are the ones in WPF and WinForms UI threads) will hang ifGetAwaiter().GetResult()
is called on non-completed task in the sync context thread, and code afterawait
operator is scheduled to be executed by the sameSynchronizationContext
. WhileConfigureAwait(false)
creates an awaitable object that forces async state machine to continue on thread pool, deadlocks are still possible. - Using additional thread. When
GetAwaiter().GetResult()
is called on non-completed task, async state machine will execute code afterawait
on a second thread, while first thread remains blocked. This may easily lead to thread-pool starvation. - Causing a deadlock on callbacks. Consider the following example:
If user code synchronously callspublic class Service { public Func<bool> Callback { get; set; } public void Execute() => ExecuteAsyncImpl().GetAwaiter().GetResult(); private async Task ExecuteAsyncImpl() { // Do some work await Task.Delay(100).ConfigureAwait(false); if (Callback()) { // Do some work await Task.Delay(100).ConfigureAwait(false); } } }
ExecuteAsync
from UI thread andCallback
also requires UI thread, there will be a deadlock:private void Button_OnClick(object sender, RoutedEventArgs e) { var dispatcher = Dispatcher.CurrentDispatcher; var service = new Service { Callback = () => dispatcher.Invoke(AskUser) }; service.Execute(); }
To avoid issues described above, GetAwaiter().GetResult()
must be called only on tasks that are already completed, which can be guaranteed only if asynchronous method is executed synchronously. For that, method should have a bool async
parameter that is either used explicitly in conditional operators to avoid asynchronous awaiting, or passed directly into other asynchronous method with bool async
parameter.
Issue Analytics
- State:
- Created 4 years ago
- Reactions:2
- Comments:5 (4 by maintainers)
Top GitHub Comments
@AlexanderSher: Given the age of this and the number of enhancements made to Azure.Core since, can you give a quick look as to whether there is still work to be done here or this issue is stale? If no longer needed, please close it out.
@pallavit : Yes; the work for this was done quite a while back. Outside of a few special cases, all the sync calls should be using
EnsureCompleted
and an analyzer is in place to enforce that convention.