Як я можу діагностувати асинхронізацію / очікування тупиків?


24

Я працюю з новою базою кодів, яка використовує async / wait. Більшість людей у ​​моїй команді також є досить новими для асинхронізації / очікування. Зазвичай ми прагнемо до кращих практик, визначених Microsoft , але, як правило, потрібен наш контекст для проходження виклику асинхронізації та працюємо з бібліотеками, які цього не роблять ConfigureAwait(false).

Поєднайте всі ці речі, і ми стикаємося з асинхронними тупиками, описаними в статті ... щотижня. Вони не відображаються під час тестування одиниць, оскільки наших знущаються джерел даних (як правило, через Task.FromResult) недостатньо, щоб запустити тупик. Тож під час тестів виконання або інтеграції деякі дзвінки служби просто виходять на обід і ніколи не повертаються. Це вбиває сервери і загалом створює безлад.

Проблема полягає в тому, що відстеження того, де була допущена помилка (як правило, просто не асинхронізуючись до кінця), як правило, включає ручну перевірку коду, яка займає багато часу і не може автоматизувати.

Що є кращим способом діагностування того, що спричинило тупик?


1
Гарне питання; Я сам це задумався. Ви читали збірку asyncстатей цього хлопця ?
Роберт Харві

@RobertHarvey - можливо, не все, але я читав деякі. Детальніше "Переконайтеся, що робити ці дві / три речі скрізь, інакше ваш код загине жахливою смертю під час виконання".
Теластин

Чи готові ви відмовитись від асинхронізації або звести її використання до найбільш вигідних моментів? Async IO - це не все або нічого.
usr

1
Якщо ви можете відтворити тупик, чи не можете ви просто переглянути слід стека, щоб побачити виклик блокування?
svick

2
Якщо проблема "не асинхронізована до кінця", то це означає, що одна половина тупикового кута є традиційним тупиком і повинна бути видна в сліді стека потоку контексту синхронізації.
svick

Відповіді:


4

Гаразд - я не впевнений, чи допоможе вам наступне, бо я зробив деякі припущення щодо розробки рішення, яке може бути, а може і не бути істинним у вашому випадку. Можливо, моє "рішення" є занадто теоретичним і працює лише на штучних прикладах - я не робив жодного тестування за межами матеріалів, наведених нижче.
Крім того, я побачив би наступне вирішення, ніж реальне рішення, але, враховуючи відсутність відповідей, я думаю, що це все-таки може бути краще, ніж нічого (я продовжував спостерігати за вашим запитанням, чекаючи рішення, але не бачачи жодного повідомлення, я почав грати навколо з питанням).

Але досить сказано: Скажімо, у нас є проста служба передачі даних, яку можна використовувати для отримання цілого числа:

public interface IDataService
{
    Task<int> LoadMagicInteger();
}

Проста реалізація використовує асинхронний код:

public sealed class CustomDataService
    : IDataService
{
    public async Task<int> LoadMagicInteger()
    {
        Console.WriteLine("LoadMagicInteger - 1");
        await Task.Delay(100);
        Console.WriteLine("LoadMagicInteger - 2");
        var result = 42;
        Console.WriteLine("LoadMagicInteger - 3");
        await Task.Delay(100);
        Console.WriteLine("LoadMagicInteger - 4");
        return result;
    }
}

Тепер проблема виникає, якщо ми використовуємо код "неправильно", як це проілюстровано цим класом. Fooнеправильно звертається Task.Resultзамість того, awaitщоб виводити результат, як Barце:

public sealed class ClassToTest
{
    private readonly IDataService _dataService;

    public ClassToTest(IDataService dataService)
    {
        this._dataService = dataService;
    }

    public async Task<int> Foo()
    {
        var result = this._dataService.LoadMagicInteger().Result;
        return result;
    }
    public async Task<int> Bar()
    {
        var result = await this._dataService.LoadMagicInteger();
        return result;
    }
}

Зараз нам (вам) потрібен спосіб написання тесту, який досягає успіху при дзвінку, Barале не вдається при виклику Foo(принаймні, якщо я правильно зрозумів питання ;-)).

Я дозволю коду говорити; ось що я придумав (використовуючи тести Visual Studio, але він також повинен працювати з використанням NUnit):

DataServiceMockвикористовує TaskCompletionSource<T>. Це дозволяє нам встановити результат у визначеній точці тестового пробігу, що призводить до наступного тесту. Зауважте, що ми використовуємо делегата для повернення TaskCompletionSource назад у тест. Ви також можете помістити це в метод ініціалізації тесту та використовувати властивості.

TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;

Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());

tcs.TrySetResult(42);

var result = task.Result;
Assert.AreEqual(42, result);

this._end = true;

Що відбувається тут, це те, що ми спочатку перевіряємо, що ми можемо залишити метод без блокування (це не спрацювало б, якщо хтось звернувся Task.Result- у цьому випадку ми зіткнулися б із тимчасовим очікуванням, оскільки результат завдання не стане доступним доти, доки метод не повернеться ).
Потім ми встановлюємо результат (тепер метод може виконати) і перевіряємо результат (всередині тесту одиниці ми можемо отримати доступ до Task.Result, оскільки ми насправді хочемо блокування).

Повний тестовий клас - BarTestуспішно і FooTestне вдається за бажанням.

[TestClass]
public class UnitTest1
{
    private DataServiceMock _dataService;
    private ClassToTest _instance;
    private bool _end;

    [TestInitialize]
    public void Initialize()
    {
        this._dataService = new DataServiceMock();
        this._instance = new ClassToTest(this._dataService);

        this._end = false;
    }
    [TestCleanup]
    public void Cleanup()
    {
        Assert.IsTrue(this._end);
    }

    [TestMethod]
    public void FooTest()
    {
        TaskCompletionSource<int> tcs = null;
        this._dataService.LoadMagicIntegerMock = t => tcs = t;

        Task<int> task = null;
        TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());

        tcs.TrySetResult(42);

        var result = task.Result;
        Assert.AreEqual(42, result);

        this._end = true;
    }
    [TestMethod]
    public void BarTest()
    {
        TaskCompletionSource<int> tcs = null;
        this._dataService.LoadMagicIntegerMock = t => tcs = t;

        Task<int> task = null;
        TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Bar());

        tcs.TrySetResult(42);

        var result = task.Result;
        Assert.AreEqual(42, result);

        this._end = true;
    }
}

І невеличкий клас помічників для тестування на тупики / тайм-аути:

public static class TaskTestHelper
{
    public static void AssertDoesNotBlock(Action action, int timeout = 1000)
    {
        var timeoutTask = Task.Delay(timeout);
        var task = Task.Factory.StartNew(action);

        Task.WaitAny(timeoutTask, task);

        Assert.IsTrue(task.IsCompleted);
    }
}

Гарна відповідь. Я планую сам спробувати ваш код, коли у мене є деякий час (я насправді не знаю напевно, спрацьовує він чи ні), але кудо і нагорода за зусилля.
Роберт Харві

-2

Ось стратегія, яку я використав у величезному і дуже, дуже багатопотоковому додатку:

По-перше, вам потрібна структура даних навколо мутексу (на жаль) і не створювати синхронізацію каталогу викликів. У цій структурі даних є посилання на будь-який раніше заблокований мютекс. Кожен мютекс має "рівень", починаючи з 0, який ви присвоюєте, коли створена мютекс і ніколи не може змінюватися.

І правило таке: Якщо мютекс заблокований, ви повинні коли-небудь блокувати інші файли на нижньому рівні. Якщо ви дотримуєтесь цього правила, то ви не можете мати тупики. Коли ви виявите порушення, ваша заявка все ще працює і працює нормально.

Коли ви виявите порушення, є дві можливості: Ви, можливо, неправильно призначили рівні. Ви заблокували A, за яким було заблоковано B, тож B повинен був мати нижчий рівень. Тож ви фіксуєте рівень і повторіть спробу.

Інша можливість: ви не можете її виправити. Деякі коди ваших блокувань A, а потім блокування B, тоді як деякі інші коди блокуються B, а потім блокування A. Немає способу призначити рівні, щоб це допустити. І звичайно, це потенційний тупик: якщо обидва коду працюють одночасно на різних потоках, є ймовірність тупикової ситуації.

Після введення цього настав доволі короткий етап, коли рівень повинен був бути скоригований, а потім тривала фаза, де були знайдені потенційні тупикові місця.


4
Вибачте, як це стосується поведінки асинхронізації / очікування? Я не можу реально ввести спеціальну структуру управління мьютексами в бібліотеку паралельних завдань.
Теластин

-3

Чи використовуєте ви Async / Await, щоб ви могли паралелізувати дорогі дзвінки, як-от до бази даних? Залежно від шляху виконання в БД, це може бути неможливим.

Тестове покриття async / await може бути складним завданням, і для пошуку помилок немає нічого подібного до реального використання виробництва. Один шаблон, який ви можете розглянути, - це передача ідентифікатора кореляції та запис його в стек, а потім каскадний тайм-аут, який записує помилку. Це більше модель SOA, але, принаймні, це дасть вам відчуття, звідки вона походить. Ми використовували це зі Splunk для пошуку тупиків.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.