考虑以下我将要同步等待的异步方法。等一下,我知道。我知道这被认为是不好的做法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
答案 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;