如何触发(不避免!)HTTPCLIENT僵局



关于如何避免使用异步代码(例如HttpClient方法)的避免僵局的问题,从同步代码中调用。我知道避免这些僵局的各种方法。

相比之下,我想了解 Gravate 触发的策略在测试过程中以错误的代码中的这些僵局。

这是最近给我们造成问题的不良代码的一个示例:

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应用程序中调用的,因此具有SynchronizationContext.Current的非null值,为潜在的死锁火提供了燃料。

除了公然滥用httpclient之外,该代码在我们公司的一台服务器中陷入僵局...但仅零星。

我试图再生僵局

我在质量检查中工作,所以我试图通过单位测试来重复僵局,该单元测试击中了提琴手的侦听器端口的本地实例:

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;
    }
}

有几件事要注意:

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

  • 我已经配置了提琴手等待一会儿(〜1分钟),然后再回复。我从同事那里听说这可能有助于再生僵局(但没有确切的证据是这种情况)。

  • 我已经使用Debugger进行了运行,以确保context是非nullthread == thread2

不幸的是,我没有运气在此单位测试中触发死锁。它总是完成,无论提琴手的延迟多长时间,除非延迟超过HttpClient的100秒默认Timeout(在这种情况下,它只是在异常时爆炸)。

我是否错过了点燃僵局火的要素?我想复制僵局,只是为了使我们的最终修复实际上有效。

看来,您认为设置任何同步上下文可能会导致使用异步代码的死锁 - 这是不正确的。在ASP.NET和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));
    }
}

它所做的只是将所有回调放在单个队列中,并在单独的单线线程中一个一个一个一个队列执行。

在这种情况下模拟僵局很容易:

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;
        }
    }
}

您无法复制该问题,因为SynchronizationContext本身不会模仿ASP.NET安装的上下文。基本SynchronizationContext没有锁定或同步,但是ASP.NET上下文确实:因为HttpContext.Current不是线程安全的,也不存储在LogicalCallContext中以在螺纹之间传递,因此AspNetSynchronizationContext对A进行了一些工作。恢复任务和b时还原HttpContext.Current。锁定以确保只有一个任务在给定的上下文中运行。

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

所在的方法是通过上下文测试您的代码,该上下文确保SendPost从未在上下文中调用。如果是这样,这表明僵局行为。要解决,要么将方法树async一直上升或在某个地方使用ConfigureAwait(false),这实际上可以从同步上下文中脱离任务完成。有关更多信息,本文详细介绍了您何时使用ConfigureAwait(false)

最新更新