133 votes

Un exemple d'async/await qui provoque une impasse

Je suis tombé sur les meilleures pratiques en matière de programmation asynchrone en utilisant le langage C#. async / await mots-clés (je suis nouveau en c# 5.0).

L'un des conseils donnés était le suivant :

Stabilité : Connaissez vos contextes de synchronisation

... Certains contextes de synchronisation sont non rentrants et monofilaires. Cela signifie qu'une seule unité de travail peut être exécutée dans le contexte à un moment donné. C'est le cas, par exemple, du thread de l'interface utilisateur Windows ou du contexte de requête ASP.NET. Dans ces contextes de synchronisation à un seul thread, il est facile de se bloquer. Si vous lancez une tâche à partir d'un contexte monofilaire, puis attendez cette tâche dans le contexte, votre code d'attente peut bloquer la tâche d'arrière-plan.

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

Si j'essaie de le disséquer moi-même, le fil de discussion principal se transforme en un nouveau fil de discussion dans le dossier de l'entreprise. MyWebService.GetDataAsync(); mais comme le fil principal attend à cet endroit, il attend le résultat dans le fichier GetDataAsync().Result . En attendant, disons que les données sont prêtes. Pourquoi le thread principal ne continue-t-il pas sa logique de continuation et ne renvoie-t-il pas un résultat de type chaîne de caractères de la part de GetDataAsync() ?

Quelqu'un peut-il m'expliquer pourquoi il y a un blocage dans l'exemple ci-dessus ? Je ne vois pas du tout quel est le problème...

97voto

Cuong Le Points 29324

Jetez un coup d'œil à cet exemple Stephen a une réponse claire pour vous :

Voici donc ce qui se passe, en commençant par la méthode de premier niveau ( Button1_Click pour UI / MyController.Get pour ASP.NET) :

  1. La méthode de premier niveau appelle GetJsonAsync (dans le contexte UI/ASP.NET).

  2. GetJsonAsync lance la requête REST en appelant HttpClient.GetStringAsync (toujours dans le contexte).

  3. GetStringAsync renvoie un Task indiquant que la demande REST n'est pas complète.

  4. GetJsonAsync attend le Task retourné par GetStringAsync . Le contexte est capturé et sera utilisé pour poursuivre l'exécution de l'opération. GetJsonAsync plus tard. GetJsonAsync renvoie un Task indiquant que le GetJsonAsync n'est pas complète.

  5. La méthode de niveau supérieur bloque de manière synchrone sur le Task retourné par GetJsonAsync . Cela bloque le fil de discussion.

  6. ... Finalement, la requête REST se termine. Ceci termine la Task qui a été renvoyé par GetStringAsync .

  7. La suite pour GetJsonAsync est maintenant prêt à s'exécuter, et il attend que le contexte soit disponible pour pouvoir s'exécuter dans le contexte.

  8. Impasse . La méthode de niveau supérieur bloque le thread de contexte, en attendant que GetJsonAsync à compléter, et GetJsonAsync attend que le contexte soit libre pour pouvoir terminer. Pour l'exemple de l'interface utilisateur, le "contexte" est le contexte de l'interface utilisateur ; pour l'exemple d'ASP.NET, le "contexte" est le contexte de la requête ASP.NET. Ce type de blocage peut être causé par l'un ou l'autre des "contextes".

Un autre lien que vous devriez lire : Attendre, et l'assurance-chômage, et les impasses ! Oh là là !

31voto

Phillip Ngan Points 4303
  • Fait 1 : GetDataAsync().Result; s'exécutera lorsque la tâche renvoyée par GetDataAsync() se termine, pendant ce temps, il bloque le thread de l'interface utilisateur.
  • Fait 2 : La poursuite de l'attente ( return result.ToString() ) est placé dans la file d'attente de l'interface utilisateur pour être exécuté.
  • Fait 3 : La tâche renvoyée par GetDataAsync() se terminera lorsque sa suite en file d'attente sera exécutée
  • Fait 4 : La continuation en file d'attente n'est jamais exécutée, car le thread de l'interface utilisateur est bloqué (fait 1).

Impasse !

On peut sortir de l'impasse en proposant des alternatives pour éviter le fait 1 ou le fait 2.

  • Évitez 1,4. Au lieu de bloquer le thread de l'interface utilisateur, utilisez var data = await GetDataAsync() ce qui permet à l'interface utilisateur de continuer à fonctionner.
  • Éviter 2,3. Mettez en file d'attente la suite de l'attente vers un autre thread qui n'est pas bloqué, par exemple en utilisant var data = Task.Run(GetDataAsync).Result qui postera la suite dans le contexte de synchronisation d'un thread du pool de threads. Cela permet à la tâche renvoyée par GetDataAsync() à compléter.

Ceci est très bien expliqué dans une article de Stephen Toub à peu près à mi-chemin où il utilise l'exemple de DelayAsync() .

30voto

Herre Kuijpers Points 389

Je viens de me pencher à nouveau sur ce problème dans un projet ASP.NET MVC. Lorsque vous voulez appeler async des méthodes d'un PartialView vous n'êtes pas autorisé à faire le PartialView async . Vous obtiendrez une exception si vous le faites.

Vous pouvez utiliser la solution de contournement simple suivante dans le scénario où vous voulez appeler un fichier de type async à partir d'une méthode de synchronisation :

  1. Avant l'appel, effacez le SynchronizationContext
  2. Faites l'appel, il n'y aura plus d'impasse ici, attendez qu'il se termine.
  3. Rétablir le SynchronizationContext

Exemple :

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

2voto

marvelTracker Points 984

Un autre point important est qu'il ne faut pas bloquer les tâches et utiliser l'asynchronisme jusqu'au bout pour éviter les blocages. Ensuite, tout sera asynchrone et non pas synchrone et bloquant.

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

0voto

Orace Points 31

J'ai trouvé un moyen de contourner ce problème en utilisant un fichier Join méthode d'extension sur la tâche avant de demander le résultat.

Le code ressemble à ceci :

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

  return View(data);
}

Où se trouve la méthode de jonction :

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

Je ne suis pas assez au fait du domaine pour voir les inconvénients de cette solution (s'il y en a).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X