在同步方法中使用 Task.Run() 以避免死锁等待异步方法?
Use Task.Run() in synchronous method to avoid deadlock waiting on async method?
UPDATE 这个问题的目的是得到一个关于Task.Run()
和死锁的简单答案。我非常理解不混合异步和同步的理论推理,并将它们牢记在心。我不甘于向他人学习新事物;我会尽可能地这样做。有时候,一个人只需要一个技术答案...
我有一个 Dispose()
方法需要调用异步方法。由于我 95% 的代码是异步的,因此重构不是最佳选择。拥有框架支持的 IAsyncDisposable
(以及其他功能)将是理想的,但我们还没有做到这一点。所以与此同时,我需要找到一种可靠的方法来从同步方法调用异步方法而不会出现死锁。
我宁愿 而不是 使用 ConfigureAwait(false)
因为这使得责任分散在我的整个代码中,让被调用者以某种方式行事,以防万一调用者是同步的。我更喜欢在同步方法中做一些事情,因为它是异常的错误。
在阅读了 Stephen Cleary 在另一个问题中的评论后,Task.Run()
总是在线程池上安排甚至异步方法,这让我思考。
在 ASP.NET 中的 .NET 4.5 或任何其他将任务调度到当前线程/同一线程的同步上下文中,如果我有异步方法:
private async Task MyAsyncMethod()
{
...
}
我想从同步方法调用它,我可以只使用 Task.Run()
和 Wait()
来避免死锁吗,因为它将异步方法排入线程池?
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();
}
如果您绝对必须 从同步方法调用异步方法,请确保在异步方法调用中使用 ConfigureAwait(false)
以避免捕获同步上下文.
这应该成立,但充其量是不稳定的。我建议考虑重构。
相反。
由于您在问题中突出显示的原因,此代码不会死锁 - 代码始终在没有同步上下文的情况下运行(因为使用线程池)并且 Wait
只会阻塞线程 till/if 方法returns.
看来你明白你的问题涉及的风险,所以我将跳过讲座。
回答您的实际问题:是的,您可以只使用 Task.Run
将该工作卸载到没有 SynchronizationContext
的 ThreadPool
线程,因此没有真正的存在死锁风险。
但是,仅仅因为它没有 SC 而使用另一个线程有点像 hack 并且可能是一个昂贵的线程,因为将工作安排在 ThreadPool
有它的成本。
IMO 更好更清晰的解决方案是暂时使用 SynchronizationContext.SetSynchronizationContext
简单地删除 SC,然后再恢复它。这可以很容易地封装到 IDisposable
中,因此您可以在 using
范围内使用它:
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);
}
}
用法:
private void MySynchronousMethodLikeDisposeForExample()
{
using (NoSynchronizationContextScope.Enter())
{
MyAsyncMethod().Wait();
}
}
使用小的自定义同步上下文,同步函数可以等待异步函数完成,而不会产生死锁。保留了原始线程,因此 sync 方法在调用 async 函数之前和之后使用相同的线程。这是 WinForms 应用程序的小示例。
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
这是我在必须同步调用异步方法时避免死锁的方法,线程可以是 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;
}
UPDATE 这个问题的目的是得到一个关于Task.Run()
和死锁的简单答案。我非常理解不混合异步和同步的理论推理,并将它们牢记在心。我不甘于向他人学习新事物;我会尽可能地这样做。有时候,一个人只需要一个技术答案...
我有一个 Dispose()
方法需要调用异步方法。由于我 95% 的代码是异步的,因此重构不是最佳选择。拥有框架支持的 IAsyncDisposable
(以及其他功能)将是理想的,但我们还没有做到这一点。所以与此同时,我需要找到一种可靠的方法来从同步方法调用异步方法而不会出现死锁。
我宁愿 而不是 使用 ConfigureAwait(false)
因为这使得责任分散在我的整个代码中,让被调用者以某种方式行事,以防万一调用者是同步的。我更喜欢在同步方法中做一些事情,因为它是异常的错误。
在阅读了 Stephen Cleary 在另一个问题中的评论后,Task.Run()
总是在线程池上安排甚至异步方法,这让我思考。
在 ASP.NET 中的 .NET 4.5 或任何其他将任务调度到当前线程/同一线程的同步上下文中,如果我有异步方法:
private async Task MyAsyncMethod()
{
...
}
我想从同步方法调用它,我可以只使用 Task.Run()
和 Wait()
来避免死锁吗,因为它将异步方法排入线程池?
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();
}
如果您绝对必须 从同步方法调用异步方法,请确保在异步方法调用中使用 ConfigureAwait(false)
以避免捕获同步上下文.
这应该成立,但充其量是不稳定的。我建议考虑重构。 相反。
由于您在问题中突出显示的原因,此代码不会死锁 - 代码始终在没有同步上下文的情况下运行(因为使用线程池)并且 Wait
只会阻塞线程 till/if 方法returns.
看来你明白你的问题涉及的风险,所以我将跳过讲座。
回答您的实际问题:是的,您可以只使用 Task.Run
将该工作卸载到没有 SynchronizationContext
的 ThreadPool
线程,因此没有真正的存在死锁风险。
但是,仅仅因为它没有 SC 而使用另一个线程有点像 hack 并且可能是一个昂贵的线程,因为将工作安排在 ThreadPool
有它的成本。
IMO 更好更清晰的解决方案是暂时使用 SynchronizationContext.SetSynchronizationContext
简单地删除 SC,然后再恢复它。这可以很容易地封装到 IDisposable
中,因此您可以在 using
范围内使用它:
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);
}
}
用法:
private void MySynchronousMethodLikeDisposeForExample()
{
using (NoSynchronizationContextScope.Enter())
{
MyAsyncMethod().Wait();
}
}
使用小的自定义同步上下文,同步函数可以等待异步函数完成,而不会产生死锁。保留了原始线程,因此 sync 方法在调用 async 函数之前和之后使用相同的线程。这是 WinForms 应用程序的小示例。
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
这是我在必须同步调用异步方法时避免死锁的方法,线程可以是 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;
}