web-dev-qa-db-fra.com

Un exemple async/wait qui provoque un blocage

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

Un des conseils donnés était le suivant:

Stabilité: Connaissez vos contextes de synchronisation

... Certains contextes de synchronisation sont non réentrants et à thread unique. Cela signifie qu'une seule unité de travail peut être exécutée dans le contexte à un moment donné. Par exemple, le thread de l'interface utilisateur Windows ou le contexte de requête ASP.NET . Dans ces contextes de synchronisation à un seul thread, il est facile de vous bloquer. Si vous générez une tâche à partir d'un contexte à un seul thread, puis attendez cette tâche dans le contexte, votre code en attente bloquera peut-être la tâche en 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 thread principal en génère un nouveau dans MyWebService.GetDataAsync();, mais comme le thread principal y attend, il attend le résultat dans GetDataAsync().Result. Pendant ce temps, disons que les données sont prêtes. Pourquoi le thread principal ne poursuit-il pas sa logique de continuation et renvoie-t-il le résultat d'une chaîne à partir de GetDataAsync()?

Quelqu'un peut-il m'expliquer s'il vous plaît pourquoi il y a une impasse dans l'exemple ci-dessus? Je ne comprends absolument pas quel est le problème ...

79
Dror Weiss

Regardez un exemple dans ici , Stephen a une réponse claire pour vous:

C'est donc ce qui se passe, en commençant par la méthode de niveau supérieur (Button1_Click for UI/MyController.Get pour ASP.NET):

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

  2. GetJsonAsync démarre la demande REST en appelant HttpClient.GetStringAsync (toujours dans le contexte).

  3. GetStringAsync renvoie une tâche non terminée, indiquant que la demande REST n'est pas terminée.

  4. GetJsonAsync attend la tâche renvoyée par GetStringAsync. Le contexte est capturé et sera utilisé pour continuer à exécuter le fichier Méthode GetJsonAsync ultérieurement. GetJsonAsync renvoie une tâche non terminée, indiquant que la méthode GetJsonAsync n'est pas complète.

  5. La méthode de niveau supérieur bloque de manière synchrone la tâche renvoyée par GetJsonAsync. Cela bloque le fil de contexte.

  6. ... Finalement, la demande REST sera terminée. Ceci termine la tâche qui a été renvoyée par GetStringAsync.

  7. La suite de GetJsonAsync est maintenant prête à être exécutée et attend que le contexte soit disponible pour pouvoir être exécuté dans le contexte.

  8. Impasse. La méthode de niveau supérieur bloque le thread de contexte, attend que GetJsonAsync soit terminée et que GetJsonAsync attend.. le contexte pour être libre afin qu'il puisse compléter. Pour l'exemple d'interface utilisateur, le fichier "contexte" est le contexte de l'interface utilisateur; pour l'exemple ASP.NET, le "contexte" est le contexte de demande ASP.NET. Ce type de blocage peut être causé pour soit "contexte".

Un autre lien que vous devriez lire:

Attends, et UI, et impasses! Oh mon!

68
cuongle
  • Fait 1: GetDataAsync().Result; sera exécuté à la fin de la tâche renvoyée par GetDataAsync(); entre temps, il bloquera le thread d'interface utilisateur.
  • Fait 2: La poursuite de l'attente (return result.ToString()) est mise en file d'attente auprès du thread d'interface utilisateur pour exécution.
  • Fait 3: La tâche renvoyée par GetDataAsync() se terminera lorsque sa continuation 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 d'interface utilisateur est bloqué (fait 1)

Impasse!

L’impasse peut être levée en proposant des solutions de rechange pour éviter les faits 1 ou 2.

  • Évitez 1,4. Au lieu de bloquer le thread d'interface utilisateur, utilisez var data = await GetDataAsync(), ce qui permet au thread d'interface utilisateur de continuer à s'exécuter
  • Évitez 2,3. Mettez en file d'attente la poursuite de l'attente vers un autre thread qui n'est pas bloqué, par exemple. utilisez var data = Task.Run(GetDataAsync).Result, qui publiera la suite dans le contexte de synchronisation d’un thread threadpool. Cela permet à la tâche renvoyée par GetDataAsync() de se terminer. 

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

11
Phillip Ngan

Je m'occupais encore de ce problème dans un projet MVC.Net. Lorsque vous souhaitez appeler des méthodes asynchrones à partir d'un PartialView, vous n'êtes pas autorisé à créer asynchrone PartialView. Vous obtiendrez une exception si vous le faites.

En résumé, une solution de contournement simple dans le scénario où vous souhaitez appeler une méthode asynchrone à partir d’une méthode de synchronisation, vous permet d’effectuer les opérations suivantes:

  1. avant l'appel, effacez le SynchronizationContext
  2. faites l'appel, il n'y aura plus d'impasse ici, attendez que cela se termine
  3. restaurer 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);
    }
10
Herre Kuijpers

Un autre point important est que vous ne devriez pas bloquer sur des tâches et utiliser asynchrone jusqu'au bout pour éviter les blocages. Ce sera alors tout blocage asynchrone et non synchrone.

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

Une solution que je viens de faire consiste à utiliser une méthode d’extension Join 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ù la méthode de jointure est:

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 dans le domaine pour voir les inconvénients de cette solution (le cas échéant)

0
Orace