Eug*_*kov 8 .net c# multithreading deadlock asynchronous
考虑以下我将同步等待的异步方法。等一下,我知道。我知道这被认为是不好的做法并导致死锁,但我完全意识到这一点,并采取措施通过用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;
}
Run Code Online (Sandbox Code Playgroud)
这段代码肯定会死锁(与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;
}
Run Code Online (Sandbox Code Playgroud)
这些“WriteInfo”行是故意的。这些调试行让我看到它偶尔发生的原因是 中的代码Task.Run,有些神秘,是由开始服务请求的同一个线程执行的。这意味着它具有 AspNetSynchronizationContext 作为SyncronizationContext并且肯定会死锁。
这是调试输出:
***(工作正常) 开始:TID:17;SCTX:System.Web.AspNetSynchronizationContext;调度程序:System.Threading.Tasks.ThreadPoolTaskScheduler 运行:TID:45;SCTX:<null> 调度程序:System.Threading.Tasks.ThreadPoolTaskScheduler 等待之前:TID:45;SCTX:<null> 调度程序:System.Threading.Tasks.ThreadPoolTaskScheduler 等待后:TID:37;SCTX:<null> 调度程序:System.Threading.Tasks.ThreadPoolTaskScheduler 第二次等待后:TID:37;SCTX:<null> 调度程序:System.Threading.Tasks.ThreadPoolTaskScheduler ***(死锁) 开始:TID:48;SCTX:System.Web.AspNetSynchronizationContext;调度程序:System.Threading.Tasks.ThreadPoolTaskScheduler 运行:TID:48;SCTX:System.Web.AspNetSynchronizationContext;调度程序:System.Threading.Tasks.ThreadPoolTaskScheduler 等待之前:TID:48;SCTX:System.Web.AspNetSynchronizationContext;调度程序:System.Threading.Tasks.ThreadPoolTaskScheduler
请注意,其中的代码在Task.Run()TID=48 的同一线程上继续运行。
问题是为什么会发生这种情况?为什么 Task.Run 在同一个线程上运行代码,允许 SyncronizationContext 仍然有效?
这是 WebAPI 控制器的完整示例代码: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();
}
Run Code Online (Sandbox Code Playgroud)
基地:TID:1;SCTX:<null> 调度程序:System.Threading.Tasks.ThreadPoolTaskScheduler T1:TID:3;SCTX:<null> 调度程序:System.Threading.Tasks.ThreadPoolTaskScheduler T2:TID:3;SCTX:<null> 调度程序:System.Threading.Tasks.ThreadPoolTaskScheduler
我们和我的一个好朋友能够通过检查堆栈跟踪和阅读 .net参考源来解决这个问题。很明显,问题的根本原因是Task.Run负载正在调用Wait任务的线程上执行。事实证明,这是 TPL 进行的性能优化,目的是不启动额外的线程并防止宝贵的线程无所事事。
以下是 Stephen Toub 的一篇文章,描述了该行为:https://blogs.msdn.microsoft.com/pfxteam/2009/10/15/task-wait-and-inlined/。
\n\n\n\n\nWait 可以简单地阻塞某些同步原语,直到目标任务完成,并且在某些情况下,\xe2\x80\x99 正是它所做的事情。\n 但是阻塞线程是一项昂贵的冒险,因为线程会占用\n大量的系统资源,并且被阻塞的线程在\xe2\x80\x99s能够继续执行有用的工作之前是很沉重的\n。相反,Wait\n 更喜欢执行有用的工作而不是阻塞,并且它有触手可及的有用工作:正在等待的任务。如果正在等待\xe2\x80\x99d 的任务已经开始执行,则等待必须阻塞。但是,如果\xe2\x80\x99t 尚未开始执行,Wait 可能能够将目标任务从其排队的调度程序中拉出,并在当前线程上内联执行它。
\n
教训:如果您确实需要同步等待异步工作,那么 Task.Run 的技巧并不可靠。你必须归零SyncronizationContext,等待,然后返回SyncronizationContext。
| 归档时间: |
|
| 查看次数: |
1445 次 |
| 最近记录: |