Un esempio async / await che causa un deadlock


94

Mi sono imbattuto in alcune best practice per la programmazione asincrona utilizzando le parole chiave async/ di c # await(sono nuovo in c # 5.0).

Uno dei consigli dati è stato il seguente:

Stabilità: conosci i tuoi contesti di sincronizzazione

... Alcuni contesti di sincronizzazione sono non rientranti e a thread singolo. Ciò significa che solo un'unità di lavoro può essere eseguita nel contesto in un dato momento. Un esempio di ciò è il thread dell'interfaccia utente di Windows o il contesto della richiesta ASP.NET. In questi contesti di sincronizzazione a thread singolo, è facile bloccarsi. Se si genera un'attività da un contesto a thread singolo, quindi si attende quell'attività nel contesto, il codice di attesa potrebbe bloccare l'attività in background.

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

Se provo a sezionarlo da solo, il thread principale viene generato in uno nuovo in MyWebService.GetDataAsync();, ma poiché il thread principale attende lì, attende il risultato in GetDataAsync().Result. Nel frattempo, diciamo che i dati sono pronti. Perché il thread principale non continua la sua logica di continuazione e restituisce un risultato di stringa da GetDataAsync()?

Qualcuno può spiegarmi perché c'è un punto morto nell'esempio sopra? Sono completamente all'oscuro di quale sia il problema ...


Sei davvero sicuro che GetDataAsync finisca le cose? O si blocca causando solo il blocco e non il deadlock?
Andrey

Questo è l'esempio fornito. Per quanto ne so, dovrebbe finire è roba e avere un risultato di qualche tipo pronto ...
Dror Weiss

4
Perché stai anche aspettando il compito? Dovresti aspettare invece perché hai praticamente perso tutti i vantaggi del modello asincrono.
Toni Petrina

Per aggiungere al punto di @ ToniPetrina, anche senza il problema del deadlock, var data = GetDataAsync().Result;c'è una riga di codice che non dovrebbe mai essere eseguita in un contesto che non dovresti bloccare (UI o richiesta ASP.NET). Anche se non si blocca, blocca il thread per un periodo di tempo indeterminato. Quindi fondamentalmente è un terribile esempio. [È necessario uscire dal thread dell'interfaccia utente prima di eseguire codice del genere, o usarlo awaitanche lì, come suggerisce Toni.]
ToolmakerSteve

Risposte:


81

Dai un'occhiata a questo esempio , Stephen ha una risposta chiara per te:

Quindi questo è ciò che accade, a partire dal metodo di primo livello ( Button1_Clickper UI / MyController.Getper ASP.NET):

  1. Le chiamate al metodo di primo livello GetJsonAsync(all'interno del contesto UI / ASP.NET).

  2. GetJsonAsyncavvia la richiesta REST chiamando HttpClient.GetStringAsync(sempre all'interno del contesto).

  3. GetStringAsyncrestituisce un messaggio incompleto Task, che indica che la richiesta REST non è stata completata.

  4. GetJsonAsyncattende il Taskreso da GetStringAsync. Il contesto viene acquisito e verrà utilizzato per continuare a eseguire il GetJsonAsyncmetodo in un secondo momento. GetJsonAsyncrestituisce un messaggio incompleto Task, indicando che il GetJsonAsyncmetodo non è completo.

  5. Il metodo di primo livello si blocca in modo sincrono sul Taskrestituito da GetJsonAsync. Questo blocca il thread di contesto.

  6. ... Alla fine, la richiesta REST verrà completata. Questo completa il messaggio Taskrestituito da GetStringAsync.

  7. La continuazione di GetJsonAsyncè ora pronta per essere eseguita e attende che il contesto sia disponibile in modo da poter essere eseguito nel contesto.

  8. Deadlock . Il metodo di primo livello sta bloccando il thread di contesto, in attesa del GetJsonAsynccompletamento e in GetJsonAsyncattesa che il contesto sia libero in modo che possa essere completato. Per l'esempio dell'interfaccia utente, il "contesto" è il contesto dell'interfaccia utente; per l'esempio ASP.NET, il "contesto" è il contesto della richiesta ASP.NET. Questo tipo di deadlock può essere causato da entrambi i "contesti".

Un altro collegamento da leggere: Await, and UI, and deadlock! Oh mio!


20
  • Fatto 1: GetDataAsync().Result;verrà eseguito al termine dell'attività restituita da GetDataAsync(), nel frattempo blocca il thread dell'interfaccia utente
  • Fatto 2: la continuazione di await ( return result.ToString()) viene accodata al thread dell'interfaccia utente per l'esecuzione
  • Fatto 3: l'attività restituita da GetDataAsync()verrà completata quando verrà eseguita la sua continuazione in coda
  • Fatto 4: la continuazione in coda non viene mai eseguita, perché il thread dell'interfaccia utente è bloccato (fatto 1)

Deadlock!

La situazione di stallo può essere risolta dalle alternative fornite per evitare il fatto 1 o il fatto 2.

  • Evita 1,4. Invece di bloccare il thread dell'interfaccia utente, usa var data = await GetDataAsync(), che consente al thread dell'interfaccia utente di continuare a funzionare
  • Evita 2,3. Accoda la continuazione dell'attesa a un thread diverso che non è bloccato, ad esempio use var data = Task.Run(GetDataAsync).Result, che inserirà la continuazione nel contesto di sincronizzazione di un thread del pool di thread. Ciò consente il completamento dell'attività restituita da GetDataAsync().

Questo è spiegato molto bene in un articolo di Stephen Toub , a circa metà strada in cui usa l'esempio di DelayAsync().


Per quanto riguarda, var data = Task.Run(GetDataAsync).Resultquesto è nuovo per me. Ho sempre pensato che l'esterno .Resultsarà prontamente disponibile non appena la prima attesa di GetDataAsyncè stata raggiunta, così datasarà sempre default. Interessante.
nawfal

19

Stavo solo giocherellando con questo problema di nuovo in un progetto ASP.NET MVC. Quando vuoi chiamare asyncmetodi da a PartialView, non ti è permesso creare PartialView async. Otterrai un'eccezione se lo fai.

È possibile utilizzare la seguente semplice soluzione alternativa nello scenario in cui si desidera chiamare un asyncmetodo da un metodo di sincronizzazione:

  1. Prima della chiamata, cancella il file SynchronizationContext
  2. Fai la chiamata, non ci sarà più deadlock qui, aspetta che finisca
  3. Ripristina il file SynchronizationContext

Esempio:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}

3

Un altro punto importante è che non dovresti bloccare su Tasks e usare async fino in fondo per evitare deadlock. Quindi sarà tutto il blocco asincrono non sincrono.

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

6
Cosa succede se voglio che il thread principale (UI) venga bloccato fino al termine dell'attività? O in un'app console, ad esempio? Diciamo che voglio usare HttpClient, che supporta solo asincrono ... Come lo uso in modo sincrono senza il rischio di deadlock ? Questo deve essere possibile. Se WebClient può essere utilizzato in questo modo (grazie ai metodi di sincronizzazione) e funziona perfettamente, perché non potrebbe essere fatto anche con HttpClient?
Dexter

Vedi la risposta di Philip Ngan sopra (so che è stata pubblicata dopo questo commento): Metti in coda la continuazione dell'attesa su un thread diverso che non è bloccato, ad esempio usa var data = Task.Run (GetDataAsync) .Result
Jeroen

@Dexter - re " E se volessi che il thread principale (UI) sia bloccato fino al termine dell'attività ? " - vuoi veramente che il thread dell'interfaccia utente sia bloccato, il che significa che l'utente non può fare nulla, non può nemmeno annullare, oppure è che non vuoi continuare il metodo in cui ti trovi? "await" o "Task.ContinueWith" gestiscono quest'ultimo caso.
ToolmakerSteve

@ToolmakerSteve ovviamente non voglio continuare il metodo. Ma semplicemente non posso usare await perché non posso nemmeno usare completamente async: HttpClient viene richiamato in main , che ovviamente non può essere async. E poi ho detto di fare tutto questo in un'app Console - in questo caso voglio esattamente il primo - non voglio nemmeno che la mia app sia multi-thread. Blocca tutto .
Dexter

-1

Un modo per aggirare il problema è usare un Joinmetodo di estensione per l'attività prima di chiedere il risultato.

Il codice ha questo aspetto:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

Dove il metodo di unione è:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

Non sono abbastanza nel dominio per vedere gli svantaggi di questa soluzione (se presente)

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.