Use Task.Run() in synchronous method to avoid deadlock waiting on async method?
It seems you understand the risks involved in your question so I'll skip the lecture.
To answer your actual question: Yes, you can just use Task.Run
to offload that work to a ThreadPool
thread which doesn't have a SynchronizationContext
and so there's no real risk for a deadlock.
However, using another thread just because it has no SC is somewhat of a hack and could be an expensive one since scheduling that work to be done on the ThreadPool
has its costs.
A better and clearer solution IMO would be to simply remove the SC for the time being using SynchronizationContext.SetSynchronizationContext
and restoring it afterwards. This can easily be encapsulated into an IDisposable
so you can use it in a using
scope:
public static class NoSynchronizationContextScope
{
public static Disposable Enter()
{
var context = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
return new Disposable(context);
}
public struct Disposable : IDisposable
{
private readonly SynchronizationContext _synchronizationContext;
public Disposable(SynchronizationContext synchronizationContext)
{
_synchronizationContext = synchronizationContext;
}
public void Dispose() =>
SynchronizationContext.SetSynchronizationContext(_synchronizationContext);
}
}
Usage:
private void MySynchronousMethodLikeDisposeForExample()
{
using (NoSynchronizationContextScope.Enter())
{
MyAsyncMethod().Wait();
}
}
Deadlock with async Task.Run method with Wait from synchronous method and timeout
You are not deadlocking the application. While Task.Run
would execute the delegate on a background thread, the following call of Task.Wait
effectively converts the originally concurrent Task.Run
code back to synchronous code: Task.Wait
and Task.Result
synchronously wait for the Task
to complete.
What you assume to be a deadlock is rather the result of a frozen main thread, caused by synchronously waiting for a long-running operation to complete.
Below the provided solutions is a section that briefly explains how Task.Result
, Task.Wait
and Task.GetAwaiter().GetResult()
can create a real deadlock.
Although the Task
related deadlock and your frozen main thread have different causes, the solution to both is similar: convert the synchronous code to an asynchronous version by introducing async/await
. This will keep the main thread responsive.
Using async/await
will replace the synchronous Task.Wait
and also make Task.Result
redundant.
The following two examples show how to convert your synchronous version to asynchronous code and provide two solutions to implement a timeout for a Task
: the first and not recommended solution uses Task.WhenAny
together with cancellation.
The second and recommended solution uses the timeout feature of the CancellationTokenSource
.
It is generally recommended to check the class API for asynchronous versions instead of using Task.Run
. In case your 3rd party library exposes an asynchronous API you should use this to replace the Task.Run
. This is because true asynchronous implementations access the kernel to use the system's hardware to execute code asynchronously without the need to create background threads.
Timeout implementation using Task.WhenAny
(not recommended)
This example uses the characteristic of Task.WhenAny
which awaits a set of Task
objects and returns the one that completes first and we usually cancel the remaining ones.
When creating a timed Task
for example by using Task.Delay
and pass it along with other Task
objects to Task.WhenAny
, we can create a race: if the timed task completes first, we can cancel the remaining Task
objects.
public async Task<ReturnMessage> FunctionAsync()
{
using var cancellationTokenSource = new CancellationTokenSource();
{
// Create the Task with cancellation support
var task = Task.Run(
() =>
{
// Check if the task needs to be cancelled
// because the timeout task ran to completion first
cancellationTokenSource.Token.ThrowIfCancellationRequested();
// It is recommended to pass a CancellationToken to the
// SyncMethod() too to allow more fine grained cancellation
var result = SyncMethod(cancellationTokenSource.Token);
return new ReturnMessage(result);
}, cancellationTokenSource.Token);
var timeout = TimeSpan.FromMilliseconds(500);
// Create a timeout Task with cancellation support
var timeoutTask = Task.Delay(timeout, cancellationTokenSource.Token);
Task firstCompletedTask = await Task.WhenAny(task, timeoutTask);
// Cancel the remaining Task that has lost the race.
cancellationTokenSource.Cancel();
if (firstCompletedTask == timeoutTask)
{
// The 'timoutTask' has won the race and has completed before the delay.
// Return an empty result.
// Because the cancellation was triggered inside this class,
// we can avoid to re-throw the OperationCanceledException
// and return an error/empty result.
return new ReturnMessage(null);
}
// The 'task' has won the race, therefore
// return its result
return await task;
}
}
Timeout implementation using the CancellationTokenSouce
constructor overload (recommended)
This example uses a specific constructor overload that accepts a TimeSpan
to configure a timeout. When the defined timeout has expired the CancellationTokeSource
will automatically cancel itself.
public async Task<ReturnsMessage> FunctionAsync()
{
var timeout = TimeSpan.FromMilliseconds(500);
using (var timeoutCancellationTokenSource = new CancellationTokenSource(timeout))
{
try
{
return await Task.Run(
() =>
{
// Check if the timeout has elapsed
timeoutCancellationTokenSource.Token.ThrowIfCancellationRequested();
// Allow every called method to invoke the cancellation
var result = SyncMethod(timeoutCancellationTokenSource.Token);
return new ReturnMessage(result);
}, timeoutCancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
// Return an empty result.
// Because the cancellation was triggered inside this class,
// we can avoid to re-throw the OperationCanceledException
// and return an error/empty result.
return new ReturnMessage(null);
}
}
}
To complete both of the above examples that converted the originally synchronous code (Function()
to asynchronous code (FunctionAsync()
), we have to await
the new method properly:
// The caller of this new asynchronous version must be await this method too.
// `await` must be used up the call tree whenever a method defined as `async`.
public async Task<ReturnMessage> void RetryableInvokeAsync()
{
ReturnMessage message = null;
for (var i = 0; i < maxAttempts; i++)
{
message = await FunctionAsync();
// Never use exceptions to control the flow.
// Control the for-loop using a condition based on the result.
if (!string.IsNullOrWhiteSpace(message.Text))
{
break;
}
}
return message;
}
Why Task.Result
, Task.Wait
and Task.GetAwaiter().GetResult()
create a deadlock when used with async
code
First, because a method is defined using the async
key word it supports asynchronous execution of operations with the help of the await
operator. Common types that support await
are Task
, Task<TResult>
, ValueTask
, ValueTask<TResult>
or any object that matches the criteria of an awaitable expression.
Inside the async
method the await
captures the SynchronizationContext
of the calling thread, the thread it is executed on. await
will also create a callback for the code that follows the await
statement, called "continuation".
This callback is enqueued on the captured SynchronizationContext
(by using SynchronizationContext.Post
) and executed once the awaitable (e.g., Task
) has signalled its completion.
Now that the callback is enqueued (stored) for later execution, await
allows the current thread to continue to do work asynchronously while executing the awaitable.
async/await
basically instructs the compiler to create a state machine.
Given is the following example that produces three potential deadlocks:
public void RetryableInvoke()
{
// Potential deadlock #1.
// Result forces the asynchronous method to be executed synchronously.
string textMessage = FunctionAsync().Result;
// Potential deadlock #2.
// Wait forces the asynchronous method to be executed synchronously.
// The caller is literally waiting for the Task to return.
textMessage = FunctionAsync().Wait();
// Potential deadlock #3.
// Task.GetAwaiter().GetResult() forces the asynchronous method to be executed synchronously.
// The caller is literally waiting for the Task to return.
textMessage = FunctionAsync().GetAwaiter().GetResult();
}
private async Task<string> FunctionAsync()
{
// Capture the SynchronizationContext of the caller's thread by awaiting the Task.
// Because the calling thread synchronously waits, the callers context is not available to process the continuation callback.
// This means that the awaited Task can't complete and return to the captured context ==> deadlock
string someText = await Task.Run(
() =>
{
/* Background thread context */
return "A message";
});
/*
Code after the await is the continuation context
and is executed on the captured SynchronizationContext of the
thread that called FunctionAsync.
In case ConfigureAwait is explicitly set to false,
the continuation is the same as the context of the background thread.
*/
// The following lines can only be executed
// after the awaited Task has successfully returned (to the captured SynchronizationContext)
someText += " More text";
return someText;
}
In the above example Task.Run
executes the delegate on a new ThreadPool
thread. Because of the await
the caller thread can continue to execute other operations instead of just waiting.
Once the Task
has signaled its completed, the pending enqueued continuation callback is ready for execution on the captured SynchronizationContext
.
This requires the caller thread to stop its current work to finish the remaining code that comes after the await
statement by executing the continuation callback.
The above example breaks the asynchronous concept by using Task.Wait
, Task.Result
and Task.GetAwaiter().GetResult()
. This Task
members will synchronously wait and therefore effectively cause the Task
to execute synchronously, which means:
a) The caller thread will block itself (wait) until the Task
is completed. Because the thread is now synchronously waiting it is not able to do anything else. If instead the invocation had been asynchronously using async/await
, then instead of waiting, the parent thread will continue to execute other operations (e.g., UI related operations in case of a UI thread) until the Task
signals its completion.
Because of the synchronous waiting the caller thread can't execute the pending continuation callback.
b) The Task
signals completion but can't return until the continuation callback has executed. The remaining code (the code after Task.Wait
) is supposed to be executed on captured SynchronizationContext
, which is the caller thread.
Since the caller thread is still synchronously waiting for the Task
to return, it can't execute the pending continuation callback.
Now the Task
must wait for the caller thread to be ready/responsive (finished with the synchronous waiting) to execute the continuation.
a) and b) describe the mutual exclusive situation that finally locks both the caller and the thread pool thread: the caller thread is waiting for the Task
, and the Task
is waiting for the caller thread. Both wait for each other indefinitely. This is the deadlock. If the caller thread is the main thread then the complete application is deadlocked and hangs.
Because the example used Task.Wait
in one place and Task.Result
in another, it has created two potential deadlock situations:
From Microsoft Docs:
Accessing the property's [
Task.Result
] get accessor blocks the calling thread until the asynchronous operation is complete; it is equivalent to calling the Wait method.
Task.GetAwaiter().GetResult()
creates the third potential deadlock.
How to fix the deadlock
To fix the deadlock we can use async/await
(recommend) or ConfigreAwait(false)
.
ConfigreAwait(true)
is the implicit default: the continuation callback is always executed on the captured SynchronizationConext
.
ConfigreAwait(false)
instructs await
(the state machine) to execute the continuation callback on the current thread pool thread of the awaited Task
. It configures await
to ignore the captured SynchronizationContext
.
ConfigreAwait(false)
basically wraps the original Task
to create a replacement Task
that causes await
to not enqueue the continuation callback on the captured SyncronizationContext
.
It's recommended to always use ConfigreAwait(false)
where executing the callback on the caller thread is not required (this is the case for library code or non UI code in general). This is because ConfigreAwait(false)
improves the performance of your code as it avoids the overhead introduced by registering the callback.ConfigreAwait(false)
should be used on the complete call tree (on every await
not only the first).
Note, there are exceptions where the callback still executes on the caller's SynchronizationContext
, for example when awaiting an already completed Task
.
public async Task RetryableInvokeAsync()
{
// Awaiting the Task always guarantees the caller thread to remain responsive.
// It can temporarily leave the context and will therefore not block it.
string textMessage = await FunctionAsync();
// **Only** if the awaited Task was configured
// to not execute the continuation on the caller's SnchronizationContext
// by using ConfigureAwait(false), the caller can use
// Wait(), GetAwaiter().GetResult() or access the Result property
// without creating a deadlock
string textMessage = FunctionAsync.Result;
}
private async Task<string> FunctionAsync()
{
// Because the awaited Task is configured to not use the caller's SynchronizationContext by using ConfigureAwait(false),
// the Task don't need to return to the captured context
// ==> no deadlock potential
string someText = await Task.Run(
() =>
{
/* Background thread context */
return "A message";
}).ConfigureAwait(false);
/*
Code after the await is the continuation context
and is not executed on the captured SynchronizationContext.
Because ConfigureAwait is explicitly set to false,
the continuation is the same as the context of the background thread.
Additionally, ConfigureAwait(false) has improved the performance
of the async code.
*/
// The following lines will always execute
// after the awaited Task has ran to completion
someText += " More text";
return someText;
}
Why access Task.Result in the synchronous mode doesn't cause a deadlock?
Result
doesn't cause a deadlock by itself. It causes a deadlock when called from a single-threaded context if there is an await
for that task that also needs that context.
More details:
await
by default captures a context and resumes on that context. (You can useConfigureAwait(false)
to override this default behavior and resume on a thread pool thread instead.)Result
blocks the current thread until thatTask
is complete. (You can useawait
to consume a task asynchronously to avoid blocking a thread.)- Some contexts are single-threaded contexts; i.e., they only allow one thread in at a time. E.g., ASP.NET Classic has a single-threaded request context. (You can use
Task.Run
to run code on a thread pool thread, with a thread pool context, which is not a single-threaded context.)
So, to get a deadlock, you need to have an await
that captures a single-threaded context, and then block a thread inside that context (e.g., calling Result
on that task). The await
needs the context to complete the Task
, but the context only allows one thread at a time, and the Result
is keeping a thread blocked in that context until the Task
completes.
In your example, you're calling GetJsonAsync
inside a Task.Run
, which runs it on the thread pool. So the await
in GetJsonAsync
(and the await
in the delegate passed to Task.Run
) capture the thread pool context, not the ASP.NET request thread context. Then your code calls Result
, which does block the ASP.NET request thread (and its context), but since the await
doesn't need that context, there's no deadlock.
Task.Run continues on the same thread causing deadlock
We with a good friend of mine were able to figure this one out via inspecting stack traces and reading .net reference source. It's evident that the root cause of problem is that Task.Run
's payload is being executed on the thread that calls Wait
on the task. As it turned out this is a performance optimization made by TPL in order not to spin up extra threads and prevent precious thread from doing nothing.
Here is an article by Stephen Toub that describes the behavior: https://blogs.msdn.microsoft.com/pfxteam/2009/10/15/task-wait-and-inlining/.
Wait could simply block on some synchronization primitive until the
target Task completed, and in some cases that’s exactly what it does.
But blocking threads is an expensive venture, in that a thread ties up
a good chunk of system resources, and a blocked thread is dead weight
until it’s able to continue executing useful work. Instead, Wait
prefers to execute useful work rather than blocking, and it has useful
work at its finger tips: the Task being waited on. If the Task being
Wait’d on has already started execution, Wait has to block. However,
if it hasn’t started executing, Wait may be able to pull the target
task out of the scheduler to which it was queued and execute it inline
on the current thread.
Lesson: If you really need to synchronously wait asynchronous work the trick with Task.Run is not reliable. You have to zero out SyncronizationContext
, wait, and then return SyncronizationContext
back.
What are the implications of running Task.Run() without Wait() in a synchronous method?
Sorry MBK, you have many questions so I need answer here:
1. When we call Task.Run() from synchronous method, does program control return to a callee's thread immediately after theTask.Run()
call?No, it depends:
- If Task has 10 threads and 0->9 queries in it's Queue then you call
Task.Run(()=>{query});
your query will be run immediately - But if Task has 10 threads and 100 queries int it's Queue then you call
Task.Run(()=>{query});
your query will not be run immediately, your query must wait for 91 orther queries run completely
I don't know exactly you mean, but if you just call Task.Run(...)
you can not know whether your query can be run successfully or not on your main thread.
For me, I will nerver use Task.Run(...)
only, I always need to know the result of query, so I will use Task.Run(...).Await() or if I don't want to block main thread, I will combine with a callback to process query result like this:
Task.Run(()=>{
try {
//Run a long running DB update query
callback.onQueryRunSuccess();
}
catch(Exception e) {
callback.onQueryRunFailed(e);
}
});
Why this async method don't cause deadlock on a thread?
You don't experience a deadlock in the first code example because of the SynchronizationContext
. This context says what the await must do to restore your code. When you start a new task (Task.Run
), you will get the default context. In the button1_click
you get the context from the form.
The context for the form allows to execute only a single thread (the one used to paint/update your form).
When you call .Wait()
, then you keep that thread 'locked' with waiting until the task is done. That means that the thread is not released for another job and this is one of the reasons that the form goes into the 'not responding' state.
When you do an await [code]
, and that code is done, then it will ask the synchronization context to schedule the remainder of the code.
- In case of the default context, any free thread is taken and your code will continue, the task is marked as done, and thus the 'loop' in the
.Wait()
gets signaled that the task is done and continues. - In case of the forms context, there is only a single thread, thus the completion of this task is scheduled to run after the current code is done. This is the reason of the deadlock.
Avoiding this deadlock is the main reason that you most often get the suggestion to use ConfigureAwait(false)
, this tells the await
that it should substitute the current synchronization context with the default one, thus allowing you to use any free thread.
The only reason you don't want to use this (or say ConfigureAwait(true)
) is if you need to update something on your form (WPF or WinForms) or you need your http context (ASP.NET, not ASP.NET Core). This is because only a single thread has access to your form or http context. Here you don't get an exception on your MessageBox.Show
because this is 'outside' of the forms context and doesn't need this special thread/synchronization context.
See https://devblogs.microsoft.com/dotnet/configureawait-faq/ for more information.
Why does Task.Run(() = something) that calls a sync method that awaits deadlock?
The problem was all the running tasks to call the sync API filled up the threadpool so that the async calls that the HttpClient had to do had no threads to run on.
Sync to async dispatch: how can I avoid deadlock?
The plugin framework is fundamentally flawed. In particular, it requires a synchronous RequestSomething
that expects to be able to call NotifyNewValueProgressAsync
to update the UI. However, it's not possible to display a UI update while the UI thread is running a synchronous method.
This forces you to use one of the most dangerous and evil sync-over-async hacks: the nested message loop hack (as briefly described in my article on brownfield async). Since this is a WPF app, you'd use a nested dispatcher frame. The main pain with this hack is that it introduces reentrancy across your entire UI layer, which is the most subtle and difficult kind of concurrency problem.
Related Topics
Return List Using Select New in Linq
Determine Label Size Based Upon Amount of Text and Font Size in Winforms/C#
Using Moq to Determine If a Method Is Called
Adding and Removing Anonymous Event Handler
Are Protected Members/Fields Really That Bad
Vb.Net Equivalent to C# Var Keyword
Display Custom Error Page When File Upload Exceeds Allowed Size in ASP.NET MVC
Mstest Cannot Find the Assembly
Translate Perl Regular Expressions to .Net
What Is Myassembly.Xmlserializers.Dll Generated For
Incorrect Stacktrace by Rethrow
Random Errors Using Wait for Element Clickable Method in Selenium
Add Custom Header in Httpwebrequest
Connecting to SQL Server with Visual Studio Express Editions
What Happens If I Return Before the End of Using Statement? Will the Dispose Be Called