it-swarm.com.de

Wie kann ich asynchrone / wartende Deadlocks diagnostizieren?

Ich arbeite mit einer neuen Codebasis, die async/await stark nutzt. Die meisten Leute in meinem Team sind auch ziemlich neu im Async/Warten. Wir halten uns im Allgemeinen an Best Practices, wie von Microsoft angegeben , benötigen jedoch im Allgemeinen unseren Kontext, um den asynchronen Aufruf zu durchlaufen, und arbeiten mit Bibliotheken, die nicht ConfigureAwait(false) sind.

Kombinieren Sie all diese Dinge und wir stoßen auf asynchrone Deadlocks, die im Artikel beschrieben werden ... wöchentlich. Sie werden beim Unit-Test nicht angezeigt, da unsere verspotteten Datenquellen (normalerweise über Task.FromResult) reichen nicht aus, um den Deadlock auszulösen. Während Laufzeit- oder Integrationstests geht ein Serviceabruf nur zum Mittagessen aus und kehrt nie zurück. Das tötet die Server und macht im Allgemeinen ein Durcheinander.

Das Problem besteht darin, dass das Aufspüren, wo der Fehler gemacht wurde (normalerweise nicht vollständig asynchron), im Allgemeinen eine manuelle Codeüberprüfung umfasst, die zeitaufwändig und nicht automatisierbar ist.

Was ist ein besserer Weg, um zu diagnostizieren, was den Deadlock verursacht hat?

24
Telastyn

Ok - Ich bin mir nicht sicher, ob das Folgende für Sie hilfreich sein wird, da ich bei der Entwicklung einer Lösung einige Annahmen getroffen habe, die in Ihrem Fall möglicherweise zutreffen oder nicht. Vielleicht ist meine "Lösung" zu theoretisch und funktioniert nur für künstliche Beispiele - ich habe keine Tests durchgeführt, die über die folgenden Dinge hinausgehen.
Außerdem würde ich das Folgende eher als Problemumgehung als als echte Lösung sehen, aber angesichts des Mangels an Antworten denke ich, dass es immer noch besser als nichts ist (ich habe Ihre Frage immer wieder auf eine Lösung gewartet, aber keine gesehen Ich habe angefangen, mit dem Problem herumzuspielen.

Aber genug gesagt: Nehmen wir an, wir haben einen einfachen Datendienst, mit dem eine Ganzzahl abgerufen werden kann:

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

Eine einfache Implementierung verwendet asynchronen Code:

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

Nun tritt ein Problem auf, wenn wir den Code "falsch" verwenden, wie in dieser Klasse dargestellt. Foo greift fälschlicherweise auf Task.Result zu, anstatt await das Ergebnis wie Bar zu bearbeiten:

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

Was wir (Sie) jetzt brauchen, ist eine Möglichkeit, einen Test zu schreiben, der beim Aufrufen von Bar erfolgreich ist, beim Aufrufen von Foo jedoch fehlschlägt (zumindest, wenn ich die Frage richtig verstanden habe ;-)).

Ich werde den Code sprechen lassen; Folgendes habe ich mir ausgedacht (mit Visual Studio-Tests, aber es sollte auch mit NUnit funktionieren):

DataServiceMock verwendet TaskCompletionSource<T>. Dies ermöglicht es uns, das Ergebnis an einem definierten Punkt im Testlauf festzulegen, der zum folgenden Test führt. Beachten Sie, dass wir einen Delegaten verwenden, um die TaskCompletionSource an den Test zurückzugeben. Sie können dies auch in die Initialize-Methode des Tests einfügen und Eigenschaften verwenden.

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;

Was hier passiert, ist, dass wir zuerst überprüfen, ob wir die Methode verlassen können, ohne sie zu blockieren (dies würde nicht funktionieren, wenn jemand auf Task.Result Zugreift - in diesem Fall würde eine Zeitüberschreitung auftreten, da das Ergebnis der Aufgabe nicht verfügbar gemacht wird bis nachdem die Methode zurückgekehrt ist).
Dann setzen wir das Ergebnis (jetzt kann die Methode ausgeführt werden) und überprüfen das Ergebnis (innerhalb eines Komponententests können wir auf Task.Result zugreifen, wenn wir tatsächlich wollen die Blockierung erfolgt). .

Vollständige Testklasse - BarTest ist erfolgreich und FooTest schlägt wie gewünscht fehl.

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

Und eine kleine Helferklasse zum Testen auf Deadlocks/Timeouts:

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);
    }
}
4
Matthias