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

Mik*_*sen 40 .net c# asynchronous task-parallel-library async-await

更新这个问题的目的是得到一个简单的答案Task.Run()和死锁.我非常理解不混合异步和同步的理论推理,我将它们铭记于心.我不是在向别人学习新事物; 我尽力做到这一点.有时候所有人都需要技术答案......

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

我宁愿使用,ConfigureAwait(false)因为这使得责任分散在我的整个代码中,以便被调用者以某种方式行事,以防调用者是同步的.我宁愿在同步方法中做一些事情,因为它是一个不正常的bugger.

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

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

private async Task MyAsyncMethod()
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

我想从同步方法中调用它,我可以使用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();
}
Run Code Online (Sandbox Code Playgroud)

i3a*_*non 58

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

回答你的实际问题:是的,你可以Task.Run用来将这项工作卸载到一个ThreadPool没有的线程中SynchronizationContext,因此没有真正的死锁风险.

但是,仅仅因为它没有SC而使用另一个线程有点像黑客并且可能是一个昂贵的,因为调度工作要做的就是ThreadPool有其成本.

一个更好,更清晰的解决方案IMO将暂时删除SC SynchronizationContext.SetSynchronizationContext并在之后恢复它.这可以很容易地封装成一个,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);
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

private void MySynchronousMethodLikeDisposeForExample()
{
    using (NoSynchronizationContextScope.Enter())
    {
        MyAsyncMethod().Wait();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 嗯......我喜欢那个.我需要这样做的地方是最小的,比如ASP.NET应用程序中的`Application_End()`,并且线程池开销可能不是一个重要因素,但这种方法肯定更直接和干净.您的回答只是"SynchronizationContext"的一个方面. (2认同)
  • @MikeJansen否.在这两种情况下都没有SC,所以在`MyAsyncMethod`中第一个`await`之后的代码将被发布到线程池,但是之前的代码等待(称为`async`方法的同步部分)将在调用线程上运行.在我的回答中,线程将是UI线程,而`Task.Run`使用线程池线程.在没有等待的情况下,或者等待任务同步完成,我的答案将只使用UI线程. (2认同)
  • 您可能需要这样做的一个很好的理由:ActonResult.Execute.没有办法使`async`,但它在请求上下文中.这非常有效. (2认同)

Dmi*_*mov 5

当我必须同步调用异步方法并且线程可以是 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;
    }
Run Code Online (Sandbox Code Playgroud)

  • 这仅适用于冷任务(那些未启动的任务)。对于已经在 UI 线程中执行的热任务,`.Result` 仍然会死锁。 (2认同)