Use Task.Run() in Synchronous Method to Avoid Deadlock Waiting on Async Method

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 use ConfigureAwait(false) to override this default behavior and resume on a thread pool thread instead.)
  • Result blocks the current thread until that Task is complete. (You can use await 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 the Task.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
2. When task is completed, does program control stay in the Task thread?

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



Leave a reply



Submit