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

时间:2015-02-03 18:18:55

标签: c# .net asynchronous task-parallel-library async-await

更新此问题的目的是获得有关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();
}

4 个答案:

答案 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