Come posso diagnosticare asincroni / attendere deadlock?

23

Sto lavorando con un nuovo codebase che fa un uso pesante di async / await. La maggior parte delle persone nel mio team sono anche abbastanza nuove per asincroni / attese. Generalmente tendiamo a tenere a Best practice come specificato da Microsoft , ma in genere è necessario che il nostro contesto scorra attraverso la chiamata asincrona e stiamo lavorando con le librerie che non contengono ConfigureAwait(false) .

Combina tutte queste cose e ci imbattiamo in deadlock asincroni descritti nell'articolo ... settimanale. Non vengono visualizzati durante il test dell'unità, perché le nostre origini dati fittizie (di solito tramite Task.FromResult ) non sono sufficienti per attivare il deadlock. Quindi durante i test di runtime o di integrazione, alcune chiamate di servizio escono a pranzo e non ritornano mai più. Che uccide i server, e generalmente fa un casino di cose.

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

Quale è un modo migliore per diagnosticare ciò che ha causato il deadlock?

    
posta Telastyn 20.10.2015 - 22:04
fonte

3 risposte

4

Ok - Non sono sicuro che quanto segue ti sarà di alcun aiuto, perché ho fatto alcune ipotesi nello sviluppo di una soluzione che può o non può essere vera nel tuo caso. Forse la mia "soluzione" è troppo teorica e funziona solo per esempi artificiali - Non ho fatto nessun test oltre a quello qui sotto.
Inoltre, vedrei più una soluzione alternativa a 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 vedendone una pubblicata ho iniziato a giocare in giro con il problema).

Ma abbastanza detto: supponiamo di avere un semplice servizio dati che può essere utilizzato per recuperare un 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, si presenta un problema, se stiamo usando il codice "erroneamente" come illustrato da questa classe. Foo accede in modo errato a Task.Result invece di await ing il risultato come Bar fa:

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 ora abbiamo bisogno è un modo per scrivere un test che ha successo quando si chiama Bar ma fallisce quando si chiama Foo (almeno se ho capito correttamente la domanda ;-)).

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

DataServiceMock utilizza TaskCompletionSource<T> . Questo ci consente di impostare il risultato in un punto definito nell'esecuzione del test che conduce al test seguente. Si noti che stiamo utilizzando un delegato per ritrasferire TaskCompletionSource nel test. Puoi anche inserire questo 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 prima verifichiamo che possiamo lasciare il metodo senza bloccare (questo non funzionerebbe se qualcuno accedesse a Task.Result - in questo caso verrebbe eseguito un timeout in quanto il risultato dell'attività non è reso disponibile fino a dopo il ritorno del metodo).
Quindi, impostiamo il risultato (ora il metodo può essere eseguito) e verifichiamo il risultato (all'interno di un test unitario possiamo accedere a Task.Result poiché in realtà vogliamo il blocco che si verifica).

Completa la classe di test - BarTest ha successo e FooTest fallisce 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);
    }
}
    
risposta data 14.12.2015 - 23:08
fonte
-2

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

Per prima cosa, hai bisogno di una struttura dati attorno a un mutex (sfortunatamente) e non fare la directory delle chiamate di sincronizzazione. In quella struttura dati, c'è un link a qualsiasi mutex precedentemente bloccato. Ogni mutex ha un "livello" che inizia da 0, che assegni quando il mutex viene creato e non può mai cambiare.

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

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

L'altra possibilità: non puoi risolverlo. Qualche codice dei tuoi blocchi A seguito dal blocco B, mentre altri codici bloccano B seguito dal blocco A. Non c'è modo di assegnare i livelli per consentire questo. E naturalmente questo è un potenziale deadlock: se entrambi i codici sono eseguiti simultaneamente su thread diversi, c'è una possibilità di deadlock.

Dopo aver introdotto questo, c'è stata una fase piuttosto breve in cui i livelli dovevano essere adeguati, seguita da una fase più lunga in cui sono stati trovati potenziali deadlock.

    
risposta data 04.11.2015 - 19:29
fonte
-3

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

La copertura del test con asincrono / attesa può essere difficile e non c'è niente come l'utilizzo della produzione reale per trovare bug. Un modello che è possibile considerare è il passaggio di un ID di correlazione e il suo log down nello stack, quindi un timeout a catena che registra l'errore. Questo è più di un modello SOA, ma almeno ti darebbe un'idea di dove proviene. Abbiamo usato questo con Splunk per trovare deadlock.

    
risposta data 09.12.2015 - 17:37
fonte

Leggi altre domande sui tag