Подтвердить что ты не робот

Как вызвать (не избегать!) Тупик HttpClient

На SO есть ряд вопросов о том, как избежать взаимоблокировок в асинхронном коде (например, методы 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, которое обеспечивало топливо для потенциального тупика.

Помимо вопиющее злоупотребление HttpClient, этот код зашел в тупик на одном из наших серверов компании... но только спорадически.

Моя попытка воспроизвести тупик

Я работаю в QA, поэтому я попытался воспроизвести тупик через unit test, который попадает в локальный экземпляр порта прослушивателя 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;
    }
}

Несколько замечаний:

  • По умолчанию unit test имеет значение null SynchronizationContext.Current, и поэтому .Result фиксирует контекст TaskScheduler, который это контекст пула потоков. Поэтому я использую SetSynchronizationContext, чтобы установить его в конкретный контекст, чтобы более точно подражать тому, что происходит в контексте ASP.NET или UI.

  • Я настроил Fiddler на некоторое время (~ 1 минута), прежде чем ответить. Я слышал от коллег, что это может помочь воспроизвести тупик (но у меня нет веских доказательств, что это так).

  • Я запускал его с помощью отладчика, чтобы убедиться, что context - не null и thread == thread2.

К сожалению, мне не повезло, вызвав тупики с помощью этого unit test. Он всегда заканчивается, независимо от того, как долго задерживается Fiddler, если задержка превышает 100-секундный по умолчанию Timeout HttpClient (в этом случае он просто взрывается с исключением).

Мне не хватает ингредиента для зажигания тупика? Я хотел бы воспроизвести тупики, чтобы быть уверенным, что наше возможное исправление действительно работает.

4b9b3361

Ответ 1

Кажется, вы считаете, что установка любого контекста синхронизации может привести к взаимоблокировке с асинхронным кодом - это неверно. Опасно блокировать асинхронный код в приложениях asp.net и UI, потому что у них есть специальный, одиночный, основной поток. В приложениях пользовательского интерфейса, который является, ну, основным потоком пользовательского интерфейса, в приложениях ASP.NET существует много таких потоков, но для данного запроса есть однопроцессорный поток.

Контексты синхронизации приложений ASP.NET и UI отличаются тем, что они в основном отправляют обратные вызовы в этот специальный поток. Поэтому, когда:

  • вы выполняете код в этом потоке
  • из этого кода вы выполняете некоторое async Task и блокируете его Result.
  • В Task есть выражение ожидания.

Появится тупик. Почему это происходит? Поскольку продолжение асинхронного метода Post ed в текущий контекст синхронизации. Те специальные контексты, которые мы обсудим выше, отправят эти продолжения в специальный основной поток. Вы уже выполняете код в этом же потоке, и он уже заблокирован - значит, тупик.

Так что ты делаешь неправильно? Во-первых, 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;
        }
    }
}

Ответ 2

Вы не смогли воспроизвести проблему, потому что SynchronizationContext сам по себе не имитирует контекст, установленный ASP.NET. База SynchronizationContext не выполняет блокировки или синхронизации, но контекст ASP.NET: Поскольку HttpContext.Current не является потокобезопасным и не хранится в LogicalCallContext, который должен быть передан между потоками, AspNetSynchronizationContext делает бит работы. восстановить HttpContext.Current при возобновлении задачи и b. lock, чтобы гарантировать выполнение только одной задачи для заданного контекста.

Аналогичная проблема существует с MVC: http://btburnett.com/2016/04/testing-an-sdk-for-asyncawait-synchronizationcontext-deadlocks.html

Приведенный здесь подход заключается в проверке вашего кода с помощью контекста, который гарантирует, что Send или Post никогда не вызывается в контексте. Если это так, это является признаком поведения взаимоблокировки. Чтобы разрешить это, либо создайте дерево методов async до конца или используйте ConfigureAwait(false) где-нибудь, что существенно отделяет завершение задачи от контекста синхронизации. Для получения дополнительной информации в этой статье описывается когда вы должны использовать ConfigureAwait(false)