C# 如何触发(而不是避免!)HttpClient死锁
关于如何避免从同步代码调用异步代码(例如,C# 如何触发(而不是避免!)HttpClient死锁,c#,async-await,deadlock,dotnet-httpclient,qa,C#,Async Await,Deadlock,Dotnet Httpclient,Qa,关于如何避免从同步代码调用异步代码(例如,HttpClient方法)中的死锁,有很多问题,如。我知道避免这些僵局的各种方法 相比之下,我想了解在测试期间加重或触发错误代码死锁的策略 下面是一个最近给我们带来问题的错误代码示例: public static string DeadlockingGet(Uri uri) { using (var http = new HttpClient()) { var response = http.GetAsync(uri).
HttpClient
方法)中的死锁,有很多问题,如。我知道避免这些僵局的各种方法
相比之下,我想了解在测试期间加重或触发错误代码死锁的策略
下面是一个最近给我们带来问题的错误代码示例:
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
,这为潜在的死锁火灾提供了燃料
除此之外,这段代码在我们公司的一台服务器上死锁了。。。但只是偶尔
我试图重新设置死锁
我在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为空。因此,我使用
将其设置为特定上下文,以便更接近地模拟ASP.NET或UI上下文中发生的情况SetSynchronizationContext
- 我已将Fiddler配置为等待一段时间(约1分钟),然后再响应。我从同事那里听说,这可能有助于重新启动僵局(但我没有确凿的证据证明情况如此)
- 我已经使用调试器运行了它,以确保
是非context
和null
thread==thread2
超时时间(在这种情况下,它只是在异常情况下爆炸)
我是不是缺少了一种可以点燃僵局之火的成分?我想重新设置死锁,只是为了确定我们最终的解决方案是否有效。您无法重现此问题,因为SynchronizationContext
本身并没有模仿ASP.NET安装的上下文。基本SynchronizationContext
不执行锁定或同步,但ASP.NET上下文执行锁定或同步:因为HttpContext.Current
不是线程安全的,也不是存储在LogicalCallContext
中,要在线程之间传递,AspNetSynchronizationContext
对线程执行一些工作。恢复任务时还原HttpContext.Current
,然后单击b。锁定以确保给定上下文中仅运行一个任务
MVC也存在类似的问题:
这里给出的方法是使用上下文测试代码,确保从不在上下文中调用Send
或Post
。如果是,则表示死锁行为。要解决此问题,请将方法树一直设置为async
,或者在某个位置使用ConfigureAwait(false),这实际上将任务完成与同步上下文分离。有关更多信息,本文详细介绍了您似乎认为设置任何同步上下文都可能会导致异步代码死锁,但事实并非如此。在asp.net和UI应用程序中阻止异步代码是危险的,因为它们具有特殊的单线程主线程。在UI应用程序中,也就是主UI线程中,ASP.NET应用程序中有许多这样的线程,但对于给定的请求,只有一个请求线程
ASP.NET和UI应用程序的同步上下文是特殊的,因为它们基本上是向一个特殊线程发送回调。因此,当:
任务
,并阻塞其结果
任务
具有等待语句Post
ed到当前同步上下文的。我们上面讨论的那些特殊上下文将把这些延续发送到特殊的主线程。您已经在同一个线程上执行了代码,并且它已经被阻塞了,因此出现了死锁
那你做错了什么?首先,SynchronizationContext
不是我们上面讨论的特殊上下文-它只是将continuations发布到线程池线程。你需要另一个来做测试。您可以使用现有的(如WindowsFormsSynchronizationContext
),也可以创建行为相同的简单上下文(示例代码,仅用于演示):
class QueueSynchronizationContext:SynchronizationContext{
private readonly BlockingCollection _queue=new BlockingCollection(new ConcurrentQueue());
公共队列同步上下文(){
新线程(()=>
{
foreach(变量项在_queue.GetConsumingEnumerable()中){
项目1(项目2);
}
}).Start();
}
公共重写void Post(sendorpostd,对象状态){
_Add(新元组(d,state));
}
公共覆盖无效发送(SendorPostD,对象状态){
//Send应该是同步的,所以我们应该在这里阻塞,但我们不会打扰
//因为这个问题不重要
_Add(新元组(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;
}
}
}
类程序{
静态void Main(字符串[]参数)
{
var ctx=新的QueueSynchronizationContext();
ctx.Send((状态)=>
{
//首先,在此上下文上执行代码
//假设您在ASP.NET请求线程中,
//或者现在在WPF UI线程中
SynchronizationContext.SetSynchronizationContext(ctx);
死锁(新Uri(“http://google.com"));
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;
}
}
}