首页 文章

在同步方法中使用Task.Run()以避免异步方法等待死锁?

提问于
浏览
34

更新这个问题的目的是得到关于 Task.Run() 和死锁的简单答案 . 我非常理解不混合异步和同步的理论推理,我将它们铭记于心 . 我只需要一个人的技术答案......

我有一个需要调用异步方法的 Dispose() 方法 . 由于95%的代码都是异步的,因此重构不是最佳选择 . 有一个 IAsyncDisposable (以及其他功能)'s supported by the framework would be ideal, but we'还没有 . 所以在同一时间,我需要找到一种可靠的方法从同步方法调用异步方法而不会发生死锁 .

我宁愿不使用 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();
}

5 回答

  • 2

    您似乎了解了问题中涉及的风险,因此我将跳过讲座 .

    要回答你的实际问题:是的,你可以使用 Task.Run 将该工作卸载到没有 SynchronizationContextThreadPool 线程,因此没有真正的死锁风险 .

    However ,使用另一个线程只是因为它没有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

    这个代码不会因为你在问题中突出显示的原因而死锁 - 代码总是在没有同步上下文的情况下运行(因为使用线程池),而 Wait 将简单地阻塞线程直到/ if方法返回 .

  • 2

    当我必须同步调用异步方法并且线程可以是UI线程时,这是我避免死锁的方法:

    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;
        }
    
  • 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
    
  • 51

    如果绝对必须从同步方法调用异步方法,请确保在异步方法调用中使用 ConfigureAwait(false) 以避免捕获同步上下文 .

    这应该成立,但充其量只是摇摇欲坠 . 我建议考虑重构 . 代替 .

相关问题