更新此问题的目的是获得有关Task.Run()
和死锁的简单答案。我非常理解不混合异步和同步的理论推理,我将它们铭记于心。我不是要向别人学习新事物;我尽力做到这一点。在所有人需要的时候,这是一个技术性答案...
我有一个需要调用异步方法的Dispose()
方法。由于95%的代码都是异步的,因此重构并不是最佳选择。拥有框架支持的IAsyncDisposable
(以及其他功能)将是理想的,但我们还没有。所以在同一时间,我需要找到一种可靠的方法来从同步方法调用异步方法而不会发生死锁。
我更喜欢而不是来使用ConfigureAwait(false)
,因为这会将责任分散在我的整个代码中,以便被调用者以某种方式行事,以防呼叫者同步。我喜欢在同步方法中做一些事情,因为它是一个不正常的bug。
在另一个Task.Run()
总是在线程池中调度甚至异步方法的问题上阅读Stephen Cleary的评论之后,它让我思考。
在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();
}
答案 0 :(得分:54)
您似乎了解了问题所涉及的风险,因此我将跳过讲座。
要回答您的实际问题:是的,您可以使用Task.Run
将该作品卸载到ThreadPool
个帖子中,该帖子没有SynchronizationContext
,所以&#39没有真正的死锁风险。
然而,因为它没有SC而使用另一个线程有点像黑客并且可能是一个昂贵的,因为在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();
}
}
答案 1 :(得分:3)
此代码不会因为您在问题中突出显示的原因而死锁 - 代码始终在没有同步上下文的情况下运行(因为使用线程池)并且Wait
将直接阻止线程直到/ if方法返回。
答案 2 :(得分:1)
如果您绝对必须从同步方法中调用异步方法,请确保在异步方法调用中使用ConfigureAwait(false)
以避免捕获同步上下文。
这应该成立,但充其量只是摇摇欲坠。我建议考虑重构。 代替。
答案 3 :(得分:1)
使用小型自定义同步上下文,同步功能可以等待异步功能的完成,而不会产生死锁。原始线程被保留,因此sync方法在调用异步函数之前和之后使用相同的线程。这是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