关于如何避免从同步代码中调用异步代码(例如,HttpClient
方法)的死锁,还有很多问题,比如this。我知道避免这些僵局的各种方法。
相比之下,我想学习在测试期间在错误代码中加重或触发这些死锁的策略。
下面是最近为我们带来问题的一段错误代码示例:
public static string DeadlockingGet(Uri uri)
{
using (var http = new HttpClient())
{
var response = http.GetAsync(uri).Result;
response.EnsureSuccessStatusCode();
return response.Content.ReadAsStringAsync().Result;
}
}
它是从一个asp.net应用程序调用的,因此其非
null
值为SynchronizationContext.Current
,这为潜在的死锁火灾提供了燃料。除了blatantly misusing HttpClient,这段代码在我们公司的一台服务器上陷入僵局…但只是偶尔的。
我试图谴责僵局
我在qa工作,所以我试图通过一个单元测试来重新设置死锁,该单元测试命中fiddler侦听器端口的本地实例:
public class DeadlockTest
{
[Test]
[TestCase("http://localhost:8888")]
public void GetTests(string uri)
{
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
var context = SynchronizationContext.Current;
var thread = Thread.CurrentThread.ManagedThreadId;
var result = DeadlockingGet(new Uri(uri));
var thread2 = Thread.CurrentThread.ManagedThreadId;
}
}
有几点需要注意:
默认情况下,单元测试有一个空的
SynchronizationContext.Current
,and so .Result
captures the context of TaskScheduler
, which is the thread pool context。因此,我使用SetSynchronizationContext
将其设置为特定上下文,以便更紧密地模拟在asp.net或ui上下文中发生的情况。我已将fiddler配置为等待一段时间(~1分钟)后再响应。我从同事那里听说,这可能有助于重振僵局(但我没有确凿的证据,事实就是这样)。
我已经用调试器运行了它,以确保
context
不是-null
和thread == thread2
。不幸的是,在这个单元测试中,我没有运气触发死锁。无论fiddler中的延迟有多长,它总是结束,除非延迟超过
Timeout
的100秒默认值HttpClient
(在这种情况下,它只是异常地爆炸)。我是不是少了一种点燃死锁之火的原料?我想重新解释一下僵局,只是要肯定我们最终的解决方案确实有效。
最佳答案
似乎您认为设置任何同步上下文都可能导致异步代码死锁—这不是真的。在asp.net和ui应用程序中阻塞异步代码是危险的,因为它们有特殊的、单一的主线程。在UI应用程序(也就是主UI线程)中,在ASP.NET应用程序中有许多这样的线程,但是对于给定的请求,只有一个请求线程。
asp.net和ui应用程序的同步上下文是特殊的,因为它们基本上是将回调发送到一个特殊线程。所以当:
在这个线程上执行一些代码
从代码中执行一些asyncTask
并阻塞它的Result
。
已经在等待声明了。
会出现死锁。为什么会这样?因为Async方法的继续被Task
绑定到当前同步上下文。我们在上面讨论的那些特殊的上下文将把这些连续体发送到特殊的主线程。您已经在同一个线程上执行了代码,它已经被阻塞,因此出现死锁。
你做错什么了?首先,Post
不是我们上面讨论过的特殊上下文-它只是将连续性发布到线程池线程。你需要另一个来测试。您既可以使用现有的(如SynchronizationContext
),也可以创建行为相同的简单上下文(示例代码,仅用于演示目的):
class QueueSynchronizationContext : SynchronizationContext {
private readonly BlockingCollection<Tuple<SendOrPostCallback, object>> _queue = new BlockingCollection<Tuple<SendOrPostCallback, object>>(new ConcurrentQueue<Tuple<SendOrPostCallback, object>>());
public QueueSynchronizationContext() {
new Thread(() =>
{
foreach (var item in _queue.GetConsumingEnumerable()) {
item.Item1(item.Item2);
}
}).Start();
}
public override void Post(SendOrPostCallback d, object state) {
_queue.Add(new Tuple<SendOrPostCallback, object>(d, state));
}
public override void Send(SendOrPostCallback d, object state) {
// Send should be synchronous, so we should block here, but we won't bother
// because for this question it does not matter
_queue.Add(new Tuple<SendOrPostCallback, object>(d, state));
}
}
它所做的只是将所有回调放在单个队列中,并在单独的单个线程上逐个执行它们。
使用此上下文模拟死锁很容易:
class Program {
static void Main(string[] args)
{
var ctx = new QueueSynchronizationContext();
ctx.Send((state) =>
{
// first, execute code on this context
// so imagine you are in ASP.NET request thread,
// or in WPF UI thread now
SynchronizationContext.SetSynchronizationContext(ctx);
Deadlock(new Uri("http://google.com"));
Console.WriteLine("No deadlock if got here");
}, null);
Console.ReadKey();
}
public static void NoDeadlock(Uri uri) {
DeadlockingGet(uri).ContinueWith(t =>
{
Console.WriteLine(t.Result);
});
}
public static string Deadlock(Uri uri)
{
// we are on "main" thread, doing blocking operation
return DeadlockingGet(uri).Result;
}
public static async Task<string> DeadlockingGet(Uri uri) {
using (var http = new HttpClient()) {
// await in async method
var response = await http.GetAsync(uri);
// this is continuation of async method
// it will be posted to our context (you can see in debugger), and will deadlock
response.EnsureSuccessStatusCode();
return response.Content.ReadAsStringAsync().Result;
}
}
}