Task.Run在同一线程上继续导致死锁 [英] Task.Run continues on the same thread causing deadlock

查看:454
本文介绍了Task.Run在同一线程上继续导致死锁的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

考虑以下我将要同步等待的异步方法.等一下,我知道.我知道这被认为是不好的做法,并且会导致死锁,但我完全有意识的并采取措施通过使用

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

我面临的问题是,即使使用这种安全"包装,它仍然偶尔会死锁.

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

            var task = taskGen();

            return task.Result;
        }).Result;
    }

这些"WriteInfo"行有意在此放置.这些调试行使我看到了它偶尔发生的原因是Task.Run中的代码有些神秘,是由开始处理请求的同一线程执行的.这意味着它具有AspNetSynchronizationContext作为 https://pastebin.com/44RP34Ye 并完整示例代码此处.

更新.这是简短的控制台应用程序代码示例,它再现了问题的根本原因-在等待的调用线程上调度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

解决方案

我们和我的一个好朋友能够通过 https://blogs.msdn.microsoft.com/pfxteam/2009/10/15/task-wait-and-inlining/.

等待可以简单地阻塞一些同步原语,直到 目标任务已完成,在某些情况下正是它的工作. 但是阻塞线程是一项昂贵的冒险,因为线程会阻塞 大量的系统资源,阻塞的线程是无用的 直到能够继续执行有用的工作为止.而是等一下 倾向于执行有用的工作,而不是阻塞,并且它很有用 用指尖操作:等待任务.如果任务正在 等待已开始执行,必须阻止.然而, 如果尚未开始执行,则Wait可能能够拉出目标 任务从排队到的调度程序中执行并内联执行 在当前线程上.

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

Consider the following async method that I'm going to wait synchronously. Wait a second, I know. I know that it's considered bad practice and causes deadlocks, but I'm fully conscious of that and taking measures to prevent deadlocks via wrapping code with 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;
    }

This code will definitely deadlock (in environments with SyncronizationContext that schedules tasks on a single thread like ASP.NET) if called like that: BadAssAsync().Result.

The problem I face is that even with this "safe" wrapper it still occasionally deadlocks.

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

            var task = taskGen();

            return task.Result;
        }).Result;
    }

These "WriteInfo" lines there in purpose. These debug lines allowed me to see that the reason why it occasionally happens is that the code within Task.Run, by some mystery, is executed by the very same thread that started serving request. It means that is has AspNetSynchronizationContext as SyncronizationContext and will definitely deadlock.

Here is debug output:

*** (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

Notice as code within Task.Run() continues on the very same thread with TID=48.

The question is why is this happening? Why Task.Run runs code on the very same thread allowing SyncronizationContext to still have an effect?

Here is the full sample code of WebAPI controller: https://pastebin.com/44RP34Ye and full sample code here.

UPDATE. Here is the shorter Console Application code sample that reproduces root cause of the issue -- scheduling Task.Run delegate on the calling thread that waits. How is that possible?

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

解决方案

We with a good friend of mine were able to figure this one out via inspecting stack traces and reading .net reference source. It's evident that the root cause of problem is that Task.Run's payload is being executed on the thread that calls Wait on the task. As it turned out this is a performance optimization made by TPL in order not to spin up extra threads and prevent precious thread from doing nothing.

Here is an article by Stephen Toub that describes the behavior: https://blogs.msdn.microsoft.com/pfxteam/2009/10/15/task-wait-and-inlining/.

Wait could simply block on some synchronization primitive until the target Task completed, and in some cases that’s exactly what it does. But blocking threads is an expensive venture, in that a thread ties up a good chunk of system resources, and a blocked thread is dead weight until it’s able to continue executing useful work. Instead, Wait prefers to execute useful work rather than blocking, and it has useful work at its finger tips: the Task being waited on. If the Task being Wait’d on has already started execution, Wait has to block. However, if it hasn’t started executing, Wait may be able to pull the target task out of the scheduler to which it was queued and execute it inline on the current thread.

Lesson: If you really need to synchronously wait asynchronous work the trick with Task.Run is not reliable. You have to zero out SyncronizationContext, wait, and then return SyncronizationContext back.

这篇关于Task.Run在同一线程上继续导致死锁的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆