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



>UPDATE 这个问题的目的是得到一个关于Task.Run()和死锁的简单答案。 我非常理解不混合异步和同步的理论推理,我把它们放在心上。 我不会从别人那里学习新事物;只要有可能,我就会这样做。 有时候,一个人所需要的只是一个技术上的答案......

我有一个需要调用异步方法的Dispose()方法。 由于我的代码中有 95% 是异步的,因此重构不是最佳选择。 拥有一个框架支持的IAsyncDisposable(以及其他功能)将是理想的,但我们还没有实现。 因此,与此同时,我需要找到一种可靠的方法来从同步方法调用异步方法而不会死锁。

我宁愿不使用ConfigureAwait(false),因为这会使责任分散在我的代码中,以便被调用方以某种方式运行,以防调用方是同步的。 我更喜欢在同步方法中做一些事情,因为它是异常的窃听器。

在阅读了 Stephen Cleary 在另一个问题中的评论后,Task.Run()总是在线程池上调度,甚至是异步方法,这让我思考。

在 .NET 4.5 中 ASP.NET 或任何其他将任务调度到当前线程/同一线程的同步上下文中,如果我有异步方法:

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();
}

看来你明白你的问题所涉及的风险,所以我会跳过讲座。

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

但是,仅仅因为它没有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();
    }
}
这是我

在必须同步调用异步方法并且线程可以是 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;
    }

由于您在问题中强调的原因,此代码不会死锁 - 代码始终在没有同步上下文的情况下运行(因为使用线程池),Wait将简单地阻止线程 till/if 方法返回。

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

这应该成立,但充其量是不稳定的。我建议考虑重构。 相反。

使用小型自定义同步上下文,同步函数可以等待异步函数完成,而不会产生死锁。保留原始线程,因此 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

最新更新