Jak zdiagnozować asynchronię / czekać na zakleszczenia?

24

Pracuję z nową bazą kodu, która intensywnie wykorzystuje async / czekaj. Większość osób w moim zespole również jest całkiem nowa, aby asynchronizować / oczekiwać. Zwykle trzymamy się najlepszych praktyk określonych przez Microsoft , ale generalnie potrzebujemy naszego kontekstu, aby przepłynąć przez wywołanie asynchroniczne i pracujemy z bibliotekami, które tego nie robią ConfigureAwait(false).

Połącz te wszystkie rzeczy i wpadniemy w impas asynchroniczny opisany w artykule ... co tydzień. Nie pojawiają się podczas testów jednostkowych, ponieważ nasze kpiące źródła danych (zwykle poprzez Task.FromResult) nie wystarczą, aby wywołać impas. Dlatego podczas testów środowiska wykonawczego lub testów integracyjnych niektóre połączenia serwisowe wychodzą na lunch i nigdy nie wracają. To zabija serwery i generalnie psuje rzeczy.

Problem polega na tym, że śledzenie miejsca, w którym popełniono błąd (zwykle po prostu brak asynchronizacji aż do samego końca), zazwyczaj wymaga ręcznej kontroli kodu, co jest czasochłonne i nie można go zautomatyzować.

Jaki jest lepszy sposób zdiagnozowania przyczyny impasu?

Telastyn
źródło
1
Dobre pytanie; Zastanawiałem się nad tym sam. Czy czytałeś kolekcję asyncartykułów tego faceta ?
Robert Harvey,
@RobertHarvey - może nie wszystkie, ale przeczytałem trochę. Więcej „Upewnij się, że robisz te dwie / trzy rzeczy wszędzie, bo inaczej twój kod umrze straszną śmiercią w czasie wykonywania”.
Telastyn,
Czy jesteś otwarty na rezygnację z asynchronizacji lub ograniczenie jej wykorzystania do najbardziej korzystnych punktów? Asynchroniczne we / wy to nie wszystko albo nic.
usr
1
Jeśli potrafisz odtworzyć impas, czy nie możesz po prostu spojrzeć na ślad stosu, aby zobaczyć wywołanie blokujące?
svick,
2
Jeśli problemem jest „nie asynchronizacja do końca”, oznacza to, że połowa impasu jest tradycyjnym impasem i powinna być widoczna w stosie wątku kontekstu synchronizacji.
svick,

Odpowiedzi:

4

Ok - nie jestem pewien, czy poniższe informacje będą dla ciebie pomocne, ponieważ poczyniłem pewne założenia przy opracowywaniu rozwiązania, które może, ale nie musi być prawdziwe w twoim przypadku. Być może moje „rozwiązanie” jest zbyt teoretyczne i działa tylko na sztuczne przykłady - nie wykonałem żadnych testów poza tymi poniżej.
Ponadto widziałbym następujące obejście problemu zamiast prawdziwego rozwiązania, ale biorąc pod uwagę brak odpowiedzi, myślę, że może być ono lepsze niż nic (obserwowałem twoje pytanie w oczekiwaniu na rozwiązanie, ale nie widząc żadnego opublikowanego, zacząłem grać wokół problemu).

Ale dość powiedziane: powiedzmy, że mamy prostą usługę danych, której można użyć do pobrania liczby całkowitej:

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

Prosta implementacja wykorzystuje kod asynchroniczny:

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

Teraz pojawia się problem, jeśli używamy kodu „niepoprawnie”, jak pokazano w tej klasie. Foonieprawidłowo dostęp Task.Resultzamiast awaiting wynik jak Barrobi:

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

To, czego teraz potrzebujemy, to sposób na napisanie testu, który się powiedzie, gdy zadzwonisz, Barale nie zadzwoni podczas rozmowy Foo(przynajmniej jeśli poprawnie zrozumiałem pytanie ;-)).

Pozwolę kodowi mówić; oto co wymyśliłem (używając testów Visual Studio, ale powinno również działać przy użyciu NUnit):

DataServiceMockwykorzystuje TaskCompletionSource<T>. To pozwala nam ustawić wynik w określonym punkcie w trakcie testu, co prowadzi do następnego testu. Zauważ, że używamy delegata, aby przekazać TaskCompletionSource z powrotem do testu. Możesz także umieścić to w metodzie Initialize testu i użyć właściwości.

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;

To, co się tutaj dzieje, polega na tym, że najpierw sprawdzamy, czy możemy opuścić metodę bez blokowania (nie zadziałałoby, gdyby ktoś uzyskał dostęp Task.Result- w tym przypadku wystąpiłby limit czasu, ponieważ wynik zadania nie został udostępniony, dopóki nie zostanie zwrócona metoda ).
Następnie ustawiamy wynik (teraz metoda może zostać wykonana) i weryfikujemy wynik (w teście jednostkowym możemy uzyskać dostęp do Task.Result, ponieważ tak naprawdę chcemy, aby nastąpiło blokowanie).

Kompletna klasa testowa - BarTestudana i FooTestnieudana zgodnie z życzeniem.

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

I mała klasa pomocnicza do testowania impasu / przekroczenia limitu czasu:

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);
    }
}
Matthias
źródło
Niezła odpowiedź. Planuję wypróbować kod sam, kiedy będę miał trochę czasu (tak naprawdę nie wiem na pewno, czy to działa, czy nie), ale podziękowania i głos za wysiłek.
Robert Harvey
-2

Oto strategia, której użyłem w ogromnej i bardzo, bardzo wielowątkowej aplikacji:

Po pierwsze, potrzebujesz struktury danych wokół muteksu (niestety) i nie twórz katalogu połączeń zsynchronizowanych. W tej strukturze danych znajduje się link do dowolnego wcześniej zablokowanego muteksu. Każdy muteks ma „poziom” rozpoczynający się od 0, który przypisujesz podczas tworzenia muteksu i nigdy się nie zmieni.

Zasada jest taka: Jeśli muteks jest zablokowany, możesz zablokować tylko inne muteksy na niższym poziomie. Jeśli zastosujesz się do tej zasady, nie będziesz mieć impasu. Gdy znajdziesz naruszenie, aplikacja nadal działa i działa poprawnie.

Gdy znajdziesz naruszenie, masz dwie możliwości: Być może źle przypisałeś poziomy. Zamknąłeś A, a następnie B, więc B powinien mieć niższy poziom. Więc popraw poziom i spróbuj ponownie.

Inna możliwość: nie można tego naprawić. Niektóre z twoich kodów blokują A, a następnie blokują B, a niektóre inne kody blokują B, a następnie blokują A. Nie ma możliwości przypisania poziomów, aby na to pozwolić. I oczywiście jest to potencjalny impas: jeśli oba kody działają jednocześnie w różnych wątkach, istnieje szansa na impas.

Po wprowadzeniu tej fazy była raczej krótka faza, w której poziomy musiały zostać dostosowane, a następnie dłuższa faza, w której znaleziono potencjalne impasy.

gnasher729
źródło
4
Przepraszam, jak to się ma do zachowania asynchronicznego / oczekiwania? Nie mogę realistycznie wstrzyknąć niestandardowej struktury zarządzania muteksami do biblioteki zadań równoległych.
Telastyn
-3

Czy używasz Async / Await, aby równolegle wykonywać drogie połączenia, np. Z bazą danych? W zależności od ścieżki wykonania w bazie danych może to nie być możliwe.

Pokrycie testowe za pomocą asynchronizacji / oczekiwania może być trudne i nie ma nic lepszego niż rzeczywiste wykorzystanie produkcji do znalezienia błędów. Jeden wzorzec, który możesz wziąć pod uwagę, to przekazywanie identyfikatora korelacji i rejestrowanie go w stosie, a następnie zastosowanie kaskadowego limitu czasu rejestrującego błąd. Jest to bardziej wzorzec SOA, ale przynajmniej dałby ci pojęcie, skąd pochodzi. Użyliśmy tego z Splunk, aby znaleźć zakleszczenia.

Robert-Ryan.
źródło