Task.Run在相同的线程上继续执行,导致死锁。

12 浏览
0 Comments

Task.Run在相同的线程上继续执行,导致死锁。

考虑下面的异步方法,我将以同步方式等待它。等一下,我知道。我知道这被认为是不好的做法,并且会导致死锁,但我完全意识到这一点,并采取措施通过使用Task.Run来防止死锁。\n以下代码在调用BadAssAsync().Result时会发生死锁(在像ASP.NET这样的仅在单线程上调度任务的环境中)。我面临的问题是,即使有这个“安全”包装,它仍然偶尔会发生死锁。\n这里的\"WriteInfo\"行是有目的的。这些调试行允许我看到偶尔发生的原因是Task.Run内的代码,以某种神秘的方式由启动服务请求的同一线程执行。这意味着它具有AspNetSynchronizationContext作为同步上下文,并且肯定会发生死锁。\n以下是调试输出:\n***(正常工作)\nSTART: TID: 17; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler\nRUN: TID: 45; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler\nBEFORE AWAIT: TID: 45; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler\nAFTER AWAIT: TID: 37; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler\nAFTER SECOND AWAIT: TID: 37; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler\n***(死锁)\nSTART: TID: 48; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler\nRUN: TID: 48; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler\nBEFORE AWAIT: TID: 48; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler\n请注意,在Task.Run()中的代码继续在TID=48的同一线程上执行。\n问题是为什么会发生这种情况?为什么Task.Run在同一线程上运行代码,允许同步上下文仍然生效?\n这是WebAPI控制器的完整示例代码:https://pastebin.com/44RP34Ye,完整示例代码在此处:http://ge.tt/2v5T1jm2。\n更新:这是一个更短的控制台应用程序代码示例,重现了问题的根本原因-在等待的调用线程上调度Task.Run委托。这是如何可能的?\nstatic void Main(string[] args)\n{\n WriteInfo(\"\\n***\\nBASE\");\n var t1 = Task.Run(() =>\n {\n WriteInfo(\"T1\");\n Task t2 = Task.Run(() =>\n {\n WriteInfo(\"T2\");\n });\n t2.Wait();\n });\n t1.Wait();\n}\nBASE: TID: 1; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler\nT1: TID: 3; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler\nT2: TID: 3; SCTX: <null> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler

0
0 Comments

在IdentityServer的内部,我发现了另一种可行的技术。它与问题中描述的不可靠技术非常相似。该源代码在本答案的末尾可以找到。

这种技术的魔力应归功于Unwrap()方法。当对Task>调用该方法时,它会创建一个新的“promise task”,该任务在执行的任务和嵌套的任务都完成后立即完成。

这种方法能够正常工作且不会导致死锁的原因很简单 - promise任务不适合内联,这是有道理的,因为没有要内联的“工作”。换句话说,这意味着我们阻塞当前线程,并且在没有同步上下文的情况下,让默认调度程序(ThreadPoolTaskScheduler)在新线程中完成工作。

此外,存在一个Task.Run的签名会隐式地进行Unwrap,从而导致下面的最短安全实现。

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

为了解决这个问题,可以使用上述的代码实现。这些代码通过创建一个新的promise任务来确保不会发生死锁,并在没有同步上下文的情况下在新线程中执行工作。

0
0 Comments

Task.Run continues on the same thread causing deadlock是由于Task.Run的payload在调用Wait的线程上执行导致的问题。这是TPL的性能优化措施,以避免额外的线程创建,防止宝贵的线程无所事事。

根据对堆栈跟踪的检查和阅读.NET引用源代码,我和我的一个好朋友解决了这个问题。这是一个由Stephen Toub撰写的文章,其中描述了这种行为。

等待可以简单地在某个同步原语上阻塞,直到目标任务完成,在某些情况下确实是这样。但是,阻塞线程是一个昂贵的操作,因为线程占用了大量的系统资源,并且阻塞的线程在能够继续执行有用的工作之前是无用的。相反,等待更倾向于执行有用的工作而不是阻塞,并且它可以通过在当前线程上内联执行等待的任务来执行有用的工作。

教训是:如果您确实需要同步等待异步工作,使用Task.Run的技巧是不可靠的。您必须将SynchronizationContext设置为null,等待,然后将SynchronizationContext恢复回来。

我的观点是,教训是技巧永远不可靠的。如果您需要手动线程管理,那么请明确地进行,并且不要依赖其他方法的副作用。

0