Task.Run继续在同一个线程上导致死锁

时间:2017-09-29 16:04:10

标签: c# .net multithreading asynchronous deadlock

考虑以下我将要同步等待的异步方法。等一下,我知道。我知道这被认为是不好的做法causes deadlocks,但我完全conscious并采取措施通过使用Task.Run包装代码来防止死锁。

    private async Task<string> BadAssAsync()
    {
        HttpClient client = new HttpClient();

        WriteInfo("BEFORE AWAIT");

        var response = await client.GetAsync("http://google.com");

        WriteInfo("AFTER AWAIT");

        string content = await response.Content.ReadAsStringAsync();

        WriteInfo("AFTER SECOND AWAIT");

        return content;
    }

这个代码肯定会死锁(在具有SyncronizationContext的环境中,在像ASP.NET这样的单个线程上调度任务)如果这样调用:BadAssAsync().Result

我面临的问题是,即使使用这种“安全”包装,它仍然偶尔会出现死锁。

    private T Wait1<T>(Func<Task<T>> taskGen)
    {
        return Task.Run(() =>
        {
            WriteInfo("RUN");

            var task = taskGen();

            return task.Result;
        }).Result;
    }

这些“WriteInfo”行有目的。这些调试行让我看到它偶尔发生的原因是Task.Run中的代码,由一些神秘的东西,由开始提供请求的同一个线程执行。这意味着AspNetSynchronizationContext为SyncronizationContext并且肯定会死锁。

这是调试输出:

*** (worked fine)
START: TID: 17; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
RUN: TID: 45; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
BEFORE AWAIT: TID: 45; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
AFTER AWAIT: TID: 37; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
AFTER SECOND AWAIT: TID: 37; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler

*** (deadlocked)
START: TID: 48; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
RUN: TID: 48; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
BEFORE AWAIT: TID: 48; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler

请注意,Task.Run()中的代码在TID = 48的同一个线程上继续。

问题是为什么会发生这种情况?为什么Task.Run在同一个线程上运行代码,允许SyncronizationContext仍然有效?

以下是WebAPI控制器的完整示例代码:https://pastebin.com/44RP34Ye和完整示例代码here

UPDATE。以下是较短的控制台应用程序代码示例,它可以重现问题的根本原因 - 在等待的调用线程上调度Task.Run委托。怎么可能?

static void Main(string[] args)
{
    WriteInfo("\n***\nBASE");

    var t1 = Task.Run(() =>
    {
        WriteInfo("T1");

        Task t2 = Task.Run(() =>
        {
            WriteInfo("T2");
        });

        t2.Wait();
    });

    t1.Wait();
}
BASE: TID: 1; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
T1: TID: 3; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
T2: TID: 3; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler

2 个答案:

答案 0 :(得分:2)

我们和我的一位好朋友能够通过inspecting stack traces并阅读.net reference source来解决这个问题。很明显,问题的根本原因是Task.Run的有效负载正在对任务调用Wait的线程上执行。事实证明,这是由TPL进行的性能优化,以便不会激活额外的线程并防止宝贵的线程无所事事。

以下是Stephen Toub撰写的一篇文章,描述了行为:https://blogs.msdn.microsoft.com/pfxteam/2009/10/15/task-wait-and-inlining/

  

等待可能只是阻塞一些同步原语,直到   目标任务已完成,在某些情况下,这正是它的作用。   但阻塞线程是一个昂贵的冒险,因为线程紧张   一大堆系统资源,一个被阻塞的线程是自重   直到它能够继续执行有用的工作。相反,等等   喜欢执行有用的工作而不是阻塞,它很有用   按指尖工作:正在等待的任务。如果任务正在   Wait'd on已经开始执行,Wait必须阻止。然而,   如果它还没有开始执行,Wait可以拉动目标   任务从排队的调度程序中排除并以内联方式执行   在当前的线程上。

课程:如果你真的需要同步等待异步工作,那么使用Task.Run的技巧是不可靠的。您必须将SyncronizationContext归零,等待,然后返回SyncronizationContext

答案 1 :(得分:0)

在IdentityServer内部,我found 另一种有效的技术。问题中提出的非可靠技术非常接近。您将在结尾处找到源代码或此答案。

使用此技术的神奇功劳应归功于Unwrap()方法。在内部针对Task<Task<T>>进行调用时,它会创建一个新的“ promise task”,该任务在完成两个任务(针对我们执行的任务和嵌套任务)后立即完成。

解决这一问题且不会造成死锁的可能性很简单-承诺任务为no subjects for inlining,这很有意义,因为没有要内联的“工作”。反过来,这意味着我们将阻止当前线程,并在没有ThreadPoolTaskScheduler的情况下让默认调度程序(SynchronizationContext)在新线程中进行工作。

internal static class AsyncHelper
{
    private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

    public static void RunSync(Func<Task> func)
    {
        _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
    }

    public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        return _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
    }
}

此外,有Task.Run的签名会隐式地执行Unwrap,这将导致以下最短的安全实现。

T SyncWait<T>(Func<Task<T>> f) => Task.Run(f).Result;