Reputation: 3476
UPDATE The purpose of this question is to get a simple answer about Task.Run()
and deadlocking. I very much understand the theoretical reasoning for not mixing async and sync, and I take them to heart. I'm not above learning new things from others; I seek to do that whenever I can. There's just times when all a guy needs is a technical answer...
I have a Dispose()
method that needs to call an async method. Since 95% of my code is async, refactoring isn't the best choice. Having an IAsyncDisposable
(among other features) that's supported by the framework would be ideal, but we're not there yet. So in the mean time, I need to find a reliable way to call async methods from a synchronous method without deadlocking.
I'd prefer not to use ConfigureAwait(false)
because that leaves the responsibility scattered all throughout my code for the callee to behave a certain way just in case the caller is synchronous. I'd prefer to do something in the synchronous method since it's the deviant bugger.
After reading Stephen Cleary's comment in another question that Task.Run()
always schedules on the thread pool even async methods, it made me think.
In .NET 4.5 in ASP.NET or any other synchronization context that schedules tasks to the current thread / same thread, if I have an asynchronous method:
private async Task MyAsyncMethod()
{
...
}
And I want to call it from a synchronous method, can I just use Task.Run()
with Wait()
to avoid deadlocks since it queues the async method the the thread pool?
private void MySynchronousMethodLikeDisposeForExample()
{
// MyAsyncMethod will get queued to the thread pool
// so it shouldn't deadlock with the Wait() ??
Task.Run((Func<Task>)MyAsyncMethod).Wait();
}
Upvotes: 61
Views: 23840
Reputation: 149538
If you absolutely must call the async method from an synchronous one, make sure to use ConfigureAwait(false)
inside your async method calls to avoid the capturing of the synchronization context.
This should hold but is shaky at best. I would advise to think of refactoring. instead.
Upvotes: 2
Reputation: 727
This is my way of avoiding deadlock when I have to call async method synchronously and the thread can be UI thread:
public static T GetResultSafe<T>(this Task<T> task)
{
if (SynchronizationContext.Current == null)
return task.Result;
if (task.IsCompleted)
return task.Result;
var tcs = new TaskCompletionSource<T>();
task.ContinueWith(t =>
{
var ex = t.Exception;
if (ex != null)
tcs.SetException(ex);
else
tcs.SetResult(t.Result);
}, TaskScheduler.Default);
return tcs.Task.Result;
}
Upvotes: 5
Reputation: 116548
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();
}
}
Upvotes: 88
Reputation: 120
With small custom synchronization context, sync function can wait for completion of async function, without creating deadlock. Original thread is preserved, so sync method use the same thread before and after call to async function. Here is small example for WinForms app.
Imports System.Threading
Imports System.Runtime.CompilerServices
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
SyncMethod()
End Sub
' waiting inside Sync method for finishing async method
Public Sub SyncMethod()
Dim sc As New SC
sc.WaitForTask(AsyncMethod())
sc.Release()
End Sub
Public Async Function AsyncMethod() As Task(Of Boolean)
Await Task.Delay(1000)
Return True
End Function
End Class
Public Class SC
Inherits SynchronizationContext
Dim OldContext As SynchronizationContext
Dim ContextThread As Thread
Sub New()
OldContext = SynchronizationContext.Current
ContextThread = Thread.CurrentThread
SynchronizationContext.SetSynchronizationContext(Me)
End Sub
Dim DataAcquired As New Object
Dim WorkWaitingCount As Long = 0
Dim ExtProc As SendOrPostCallback
Dim ExtProcArg As Object
<MethodImpl(MethodImplOptions.Synchronized)>
Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
Interlocked.Increment(WorkWaitingCount)
Monitor.Enter(DataAcquired)
ExtProc = d
ExtProcArg = state
AwakeThread()
Monitor.Wait(DataAcquired)
Monitor.Exit(DataAcquired)
End Sub
Dim ThreadSleep As Long = 0
Private Sub AwakeThread()
If Interlocked.Read(ThreadSleep) > 0 Then ContextThread.Resume()
End Sub
Public Sub WaitForTask(Tsk As Task)
Dim aw = Tsk.GetAwaiter
If aw.IsCompleted Then Exit Sub
While Interlocked.Read(WorkWaitingCount) > 0 Or aw.IsCompleted = False
If Interlocked.Read(WorkWaitingCount) = 0 Then
Interlocked.Increment(ThreadSleep)
ContextThread.Suspend()
Interlocked.Decrement(ThreadSleep)
Else
Interlocked.Decrement(WorkWaitingCount)
Monitor.Enter(DataAcquired)
Dim Proc = ExtProc
Dim ProcArg = ExtProcArg
Monitor.Pulse(DataAcquired)
Monitor.Exit(DataAcquired)
Proc(ProcArg)
End If
End While
End Sub
Public Sub Release()
SynchronizationContext.SetSynchronizationContext(OldContext)
End Sub
End Class
Upvotes: 1
Reputation: 100547
This code will not deadlock for exactly the reasons you highlighted in the question - code always runs with no synchronization context (since using thread pool) and Wait
will simply block the thread till/if method returns.
Upvotes: 3