Un esempio asincrono / atteso che causa un deadlock

Mi sono imbattuto in alcune best practice per la programmazione asincrona usando le parole chiave asincrone / attese di c # (sono nuovo nel 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 single-threaded. Ciò significa che solo una unità di lavoro può essere eseguita nel contesto in un dato momento. Un esempio di questo è il thread dell’interfaccia utente di Windows o il contesto di richiesta di ASP.NET. In questi contesti di sincronizzazione a thread singolo, è facile bloccarsi. Se si genera un’attività da un contesto a thread singolo, quindi attendere tale 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 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 (). Nel frattempo, dite che i dati sono pronti. Perché il thread principale non continua la sua logica di continuazione e restituisce un risultato stringa da GetDataAsync ()?

Qualcuno può spiegarmi perché c’è un deadlock nell’esempio sopra? Sono completamente all’oscuro di qual è il problema …

Guarda un esempio qui , Stephen ha una risposta chiara per te:

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

  1. Il metodo di livello superiore chiama GetJsonAsync (all’interno del contesto UI / ASP.NET).

  2. GetJsonAsync avvia la richiesta REST chiamando HttpClient.GetStringAsync (sempre all’interno del contesto).

  3. GetStringAsync restituisce un’attività non completata, indicando che la richiesta REST non è completa.

  4. GetJsonAsync attende l’attività restituita da GetStringAsync. Il contesto viene catturato e verrà utilizzato per continuare a eseguire successivamente il metodo GetJsonAsync. GetJsonAsync restituisce un’attività non completata, a indicare che il metodo GetJsonAsync non è completo.

  5. Il metodo di livello superiore blocca in modo sincrono sull’attività restituita da GetJsonAsync. Questo blocca il thread di contesto.

  6. … Alla fine, la richiesta REST verrà completata. Questo completa l’attività che è stata restituita da GetStringAsync.

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

  8. Deadlock. Il metodo di livello superiore sta bloccando il thread di contesto, in attesa del completamento di GetJsonAsync e GetJsonAsync sta aspettando che il contesto sia libero affinché 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 per “contesto”.

Un altro link da leggere:

Attese, interfaccia utente e deadlock! Oh mio!

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

Deadlock!

Il deadlock può essere rotto da alternative fornite per evitare Fact 1 o Fact 2.

  • Evita 1,4. Invece di bloccare il thread dell’interfaccia utente, utilizza var data = await GetDataAsync() , che consente al thread dell’interfaccia utente di continuare a funzionare
  • Evita 2,3. Coda la continuazione dell’attesa su un thread diverso che non è bloccato, ad esempio usa var data = Task.Run(GetDataAsync).Result , che invierà la continuazione al contesto di sincronizzazione di un thread del thread. Ciò consente di completare l’attività restituita da GetDataAsync() .

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

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

 public async Task ActionAsync() { var data = await GetDataAsync(); return View(data); } private async Task GetDataAsync() { // a very simple async method var result = await MyWebService.GetDataAsync(); return result.ToString(); } 

Stavo solo riprendendo questo problema in un progetto MVC.Net. Quando si desidera chiamare i metodi asincroni da un PartialView, non è ansible rendere il PartialView async. Avrai un’eccezione se lo fai.

Quindi, fondamentalmente una soluzione semplice nello scenario in cui si desidera chiamare un metodo asincrono da un metodo di sincronizzazione, è ansible effettuare le seguenti operazioni:

  1. prima della chiamata, deselezionare SynchronizationContext
  2. fai la chiamata, non ci saranno più deadlock qui, aspetta che finisca
  3. ripristinare il 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); }