如何触发(不要避免!)HttpClient死锁

Dum*_*fus 13 c# deadlock qa async-await dotnet-httpclient

关于如何避免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;
    }
}
Run Code Online (Sandbox Code Playgroud)

它是从一个ASP.NET应用程序调用的,因此具有非nullSynchronizationContext.Current,它为潜在的僵局火灾提供了燃料.

除了公然滥用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;
    }
}
Run Code Online (Sandbox Code Playgroud)

有几点需要注意:

  • 默认情况下,单元测试具有null SynchronizationContext.Current,因此.Result捕获上下文TaskScheduler,即线程池上下文.因此,我使用SetSynchronizationContext它将其设置为特定的上下文,以更接近地模拟ASP.NET或UI上下文中发生的事情.

  • 我已经将Fiddler配置为等待一段时间(约1分钟)然后才回复.我从同事那里听说这可能有助于重新制造僵局(但我没有确凿证据证明这种情况).

  • 我用调试器运行它以确保它context是非nullthread == thread2.

不幸的是,我没有运气通过这个单元测试触发死锁.它总是能完成,无论在提琴手延迟多久,除非延迟超过100秒的默认TimeoutHttpClient(在这种情况下,它只是吹了一个例外).

我错过了点燃死锁火的成分吗?我想重新解决僵局,只是为了肯定我们的最终修复确实有效.

Evk*_*Evk 8

您似乎认为设置任何同步上下文可能会导致异步代码死锁 - 这不是真的.阻止asp.net和UI应用程序中的异步代码是危险的,因为它们具有特殊的单一主线程.在UI应用程序中,主UI空间线程,在ASP.NET应用程序中有许多这样的线程,但对于给定的请求,有一个 - 请求线程.

ASP.NET和UI应用程序的同步上下文的特殊之处在于它们基本上将回调发送到该特殊线程.所以当:

  1. 你在这个线程上执行一些代码
  2. 从该代码中执行一些异步Task并阻塞它Result.
  3. 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));
    }
}
Run Code Online (Sandbox Code Playgroud)

它所做的就是将所有回调放到单个队列中,并在单独的单线程上逐个执行.

使用此上下文模拟死锁很容易:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Dar*_*con 4

您无法重现该问题,因为SynchronizationContext它本身并不模仿 ASP.NET 安装的上下文。基础SynchronizationContext不执行锁定或同步,但 ASP.NET 上下文执行:因为HttpContext.Current不是线程安全的,也不是存储在LogicalCallContext要在线程之间传递的 中,所以AspNetSynchronizationContext对 a 做了一些工作。HttpContext.Current恢复任务时恢复 b .锁定以确保给定上下文中只有一个任务正在运行。

MVC 也存在类似的问题:http://btburnett.com/2016/04/testing-an-sdk-for-asyncawait-synchronizationcontext-deadlocks.html

那里给出的方法是使用上下文测试您的代码,以确保永远不会在上下文上调用Sendor 。Post如果是,则表明存在死锁行为。要解决这个问题,要么将方法树async一直向上,要么ConfigureAwait(false)在某个地方使用,这本质上将任务完成与同步上下文分离。有关更多信息,本文详细介绍了何时应使用ConfigureAwait(false)