有很多关于如何避免在同步代码中调用异步代码(例如,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
(在这种情况下,它会因异常而爆炸)。
我错过了点燃死锁火的成分吗?我想重新制定僵局,只是为了肯定我们的最终解决方案确实有效。
答案 0 :(得分:8)
您似乎认为设置任何同步上下文可能会导致异步代码死锁 - 这不是真的。阻止asp.net和UI应用程序中的异步代码是危险的,因为它们具有特殊的单一主线程。在UI应用程序中,主UI空间线程,在ASP.NET应用程序中有许多这样的线程,但对于给定的请求,有一个 - 请求线程。
ASP.NET和UI应用程序的同步上下文非常特殊,因为它们基本上将回调发送到该特殊线程。所以当:
Task
并阻止其Result
。Task
等待声明。将发生死锁。为什么会这样?因为异步方法的延续是Post
到当前同步上下文。我们上面讨论的那些特殊上下文将把这些延续发送到特殊的主线程。你已经在同一个线程上执行了代码并且它已被阻止 - 因此死锁。
那你做错了什么?首先,SynchronizationContext
不是我们上面讨论过的特殊上下文 - 它只是将线程池线程的延续发布。你需要另一个进行测试。您可以使用现有的(例如WindowsFormsSynchronizationContext
),也可以创建行为相同的简单上下文(示例代码,仅用于演示目的):
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;
}
}
}
答案 1 :(得分:4)
您无法重现此问题,因为SynchronizationContext
本身并不模仿ASP.NET安装的上下文。基础SynchronizationContext
没有锁定或同步,但是ASP.NET上下文会这样做:因为HttpContext.Current
不是线程安全的,也不存储在线程之间传递的LogicalCallContext
中, AspNetSynchronizationContext
做了一些工作。恢复任务时恢复HttpContext.Current
和b。锁定以确保只有一个任务正在为给定的上下文运行。
MVC存在类似的问题:http://btburnett.com/2016/04/testing-an-sdk-for-asyncawait-synchronizationcontext-deadlocks.html
给出的方法是使用上下文测试代码,确保永远不会在上下文中调用Send
或Post
。如果是,则表明死锁行为。要解决此问题,请将方法树async
一直向上或在某处使用ConfigureAwait(false)
,这实质上将任务完成与同步上下文分离。有关详细信息,请参阅此文章的详细信息when you should use ConfigureAwait(false)