Come posso diagnosticare deadlock asincroni / in attesa?


24

Sto lavorando con un nuovo codebase che fa un uso intensivo di async / waitit. La maggior parte delle persone del mio team sono anche abbastanza nuove da asincrare / attendere. Generalmente tendiamo a seguire le Best Practices come specificato da Microsoft , ma generalmente abbiamo bisogno che il nostro contesto scorra attraverso la chiamata asincrona e che stiamo lavorando con librerie che non lo fanno ConfigureAwait(false).

Combina tutte queste cose e ci imbattiamo in deadlock asincroni descritti nell'articolo ... settimanalmente. Non vengono visualizzati durante i test delle unità, perché le nostre origini dati derise (di solito tramite Task.FromResult) non sono sufficienti per attivare il deadlock. Pertanto, durante i test di runtime o di integrazione, alcune chiamate di servizio vanno solo a pranzo e non ritornano mai. Ciò uccide i server e generalmente crea confusione.

Il problema è che rintracciare il punto in cui è stato commesso l'errore (di solito non è completamente asincrono) comporta generalmente un'ispezione manuale del codice, che richiede tempo e non è automatizzabile.

Qual è un modo migliore per diagnosticare ciò che ha causato lo stallo?


1
Buona domanda; Me lo sono chiesto da solo. Hai letto la raccolta di asyncarticoli di questo ragazzo ?
Robert Harvey,

@RobertHarvey - forse non tutto, ma ne ho letti alcuni. Altro "Assicurati di fare queste due / tre cose ovunque o altrimenti il ​​tuo codice morirà di una morte orribile in fase di esecuzione".
Telastyn,

Sei disposto a lasciare asincrono o a ridurne l'utilizzo ai punti più vantaggiosi? L'IO asincrono non è tutto o niente.
usr

1
Se riesci a riprodurre il deadlock, non puoi semplicemente guardare la traccia dello stack per vedere la chiamata di blocco?
svick

2
Se il problema non è "asincrono fino in fondo", significa che metà del deadlock è un deadlock tradizionale e dovrebbe essere visibile nella traccia dello stack del thread di contesto di sincronizzazione.
svick

Risposte:


4

Ok - Non sono sicuro che quanto segue possa esserti di aiuto, perché ho formulato alcune ipotesi nello sviluppo di una soluzione che potrebbe o meno essere vera nel tuo caso. Forse la mia "soluzione" è troppo teorica e funziona solo per esempi artificiali - non ho fatto alcun test oltre alle cose di seguito.
Inoltre, vedrei quanto segue più una soluzione alternativa che una soluzione reale, ma considerando la mancanza di risposte penso che potrebbe essere ancora meglio di niente (ho continuato a guardare la tua domanda in attesa di una soluzione, ma non vedendo uno che veniva pubblicato ho iniziato a giocare in giro con il problema).

Ma abbastanza detto: diciamo che abbiamo un semplice servizio dati che può essere utilizzato per recuperare un numero intero:

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

Una semplice implementazione utilizza un codice asincrono:

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

Ora, sorge un problema, se stiamo usando il codice "in modo errato" come illustrato da questa classe. Fooaccede in modo errato Task.Resultinvece di awaiting il risultato come Barfa:

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

Ciò di cui noi (tu) ora abbiamo bisogno è un modo per scrivere un test che abbia successo quando chiama Barma fallisce quando chiama Foo(almeno se ho capito bene la domanda ;-)).

Lascio parlare il codice; ecco cosa mi è venuto in mente (usando i test di Visual Studio, ma dovrebbe funzionare anche con NUnit):

DataServiceMockutilizza TaskCompletionSource<T>. Questo ci consente di impostare il risultato in un punto definito nell'esecuzione del test che porta al seguente test. Si noti che stiamo utilizzando un delegato per restituire TaskCompletionSource al test. Puoi anche inserirlo nel metodo Initialize del test e utilizzare le proprietà.

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;

Quello che sta succedendo qui è che per prima cosa verifichiamo che possiamo lasciare il metodo senza bloccare (questo non funzionerebbe se qualcuno accedesse Task.Result- in questo caso avremmo un timeout poiché il risultato dell'attività non è reso disponibile fino a quando il metodo non è tornato ).
Quindi, impostiamo il risultato (ora il metodo può essere eseguito) e verifichiamo il risultato (all'interno di un unit test possiamo accedere a Task.Result come vogliamo realmente che si verifichi il blocco).

Classe di test completa: ha esito BarTestpositivo e FooTestnegativo come desiderato.

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

E una piccola classe di supporto per testare deadlock / timeout:

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

Bella risposta. Sto programmando di provare il tuo codice da solo quando avrò un po 'di tempo (in realtà non so per certo se funziona o meno), ma complimenti e un voto positivo per lo sforzo.
Robert Harvey,

-2

Ecco una strategia che ho usato in un'applicazione enorme e molto, molto multithread:

Innanzitutto, è necessario disporre di una struttura di dati attorno a un mutex (purtroppo) e non effettuare la sincronizzazione delle directory delle chiamate. In quella struttura di dati, c'è un collegamento a qualsiasi mutex precedentemente bloccato. Ogni mutex ha un "livello" che inizia da 0, che si assegna quando viene creato il mutex e non può mai cambiare.

E la regola è: se un mutex è bloccato, devi sempre e solo bloccare altri mutex a un livello inferiore. Se segui questa regola, non puoi avere deadlock. Quando trovi una violazione, l'applicazione è ancora attiva e funzionante.

Quando trovi una violazione, ci sono due possibilità: potresti aver assegnato i livelli errati. Hai bloccato A seguito dal blocco B, quindi B avrebbe dovuto avere un livello inferiore. Quindi correggi il livello e riprova.

L'altra possibilità: non è possibile risolverlo. Alcuni dei tuoi blocchi di codice A seguiti dal blocco B, mentre altri blocchi di codice B seguiti dal blocco A. Non c'è modo di assegnare i livelli per consentirlo. E ovviamente questo è un potenziale deadlock: se entrambi i codici vengono eseguiti simultaneamente su thread diversi, c'è una possibilità di deadlock.

Dopo aver introdotto questo, c'è stata una fase piuttosto breve in cui è stato necessario regolare i livelli, seguita da una fase più lunga in cui sono stati rilevati potenziali deadlock.


4
Mi dispiace, come si applica al comportamento asincrono / in attesa? Non riesco a iniettare realisticamente una struttura di gestione del mutex personalizzata nella Task Parallel Library.
Telastyn,

-3

Stai usando Async / Await in modo da poter parallelizzare chiamate costose come quelle di un database? A seconda del percorso di esecuzione nel DB, ciò potrebbe non essere possibile.

La copertura dei test con asincrono / attende può essere impegnativa e non c'è niente come l'uso reale della produzione per trovare i bug. Un modello che potresti prendere in considerazione è passare un ID di correlazione e registrarlo nello stack, quindi avere un timeout a cascata che registra l'errore. Questo è più un modello SOA ma almeno ti darebbe un'idea di dove proviene. Lo abbiamo usato con Splunk per trovare deadlock.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.