Un exemple async / await qui provoque un blocage

95

Je suis tombé sur quelques bonnes pratiques pour la programmation asynchrone en utilisant les mots async- awaitclés / c # (je suis nouveau sur c # 5.0).

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

Stabilité: connaissez vos contextes de synchronisation

... Certains contextes de synchronisation sont non réentrants et monothread. Cela signifie qu'une seule unité de travail peut être exécutée dans le contexte à un moment donné. Le thread d'interface utilisateur Windows ou le contexte de demande ASP.NET en est un exemple. Dans ces contextes de synchronisation à un seul thread, il est facile de se bloquer soi-même. Si vous créez une tâche à partir d'un contexte à thread unique, puis attendez cette tâche dans le contexte, votre code d'attente peut bloquer 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 apparaît dans un nouveau MyWebService.GetDataAsync();, mais comme le thread principal y attend, il attend le résultat GetDataAsync().Result. En attendant, disons que les données sont prêtes. Pourquoi le thread principal ne poursuit-il pas sa logique de continuation et renvoie-t-il un résultat de chaîne GetDataAsync()?

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

Dror Weiss
la source
Etes-vous vraiment sûr que GetDataAsync termine ses trucs? Ou il reste bloqué causant simplement un verrouillage et non une impasse?
Andrey
C'est l'exemple qui a été fourni. À ma connaissance, il devrait finir ses trucs et avoir un résultat prêt ...
Dror Weiss
4
Pourquoi attendez-vous même la tâche? Vous devriez plutôt attendre car vous avez pratiquement perdu tous les avantages du modèle asynchrone.
Toni Petrina
Pour ajouter au point de @ ToniPetrina, même sans le problème de blocage, var data = GetDataAsync().Result;est une ligne de code qui ne devrait jamais être faite dans un contexte que vous ne devriez pas bloquer (requête UI ou ASP.NET). Même s'il ne se bloque pas, il bloque le thread pour une durée indéterminée. Donc, fondamentalement, c'est un exemple terrible. [Vous devez quitter le thread de l'interface utilisateur avant d'exécuter un code comme celui-ci, ou l'utiliser awaitaussi, comme le suggère Toni.]
ToolmakerSteve

Réponses:

81

Jetez un œ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 niveau supérieur ( Button1_Clickpour UI / MyController.Getpour ASP.NET):

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

  2. GetJsonAsyncdémarre la requête REST en appelant HttpClient.GetStringAsync(toujours dans le contexte).

  3. GetStringAsyncrenvoie une Taskrequête incomplète , indiquant que la requête REST n'est pas terminée.

  4. GetJsonAsyncattend le Taskretour par GetStringAsync. Le contexte est capturé et sera utilisé pour continuer à exécuter la GetJsonAsyncméthode ultérieurement. GetJsonAsyncrenvoie un incomplet Task, indiquant que la GetJsonAsyncméthode n'est pas terminée.

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

  6. ... Finalement, la demande REST se terminera. Ceci complète le Taskqui a été renvoyé par GetStringAsync.

  7. La continuation de GetJsonAsyncest maintenant prête à être exécutée et elle 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, attend la GetJsonAsyncfin et GetJsonAsyncattend que le contexte soit libre pour pouvoir se terminer. Pour l'exemple d'interface utilisateur, le «contexte» est le contexte d'interface utilisateur; pour l'exemple ASP.NET, le «contexte» est le contexte de la requête ASP.NET. Ce type de blocage peut être causé pour l'un ou l'autre "contexte".

Un autre lien que vous devriez lire: Attendez, et l'interface utilisateur et les blocages! Oh mon!

cuongle
la source
21
  • Fait 1: GetDataAsync().Result;s'exécutera lorsque la tâche retournée par sera GetDataAsync()terminée, en attendant, il bloque le thread de l'interface utilisateur
  • Fait 2: La continuation de await ( return result.ToString()) est mise en file d'attente dans le thread de l'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!

Le blocage peut être brisé par des alternatives fournies pour éviter le fait 1 ou le fait 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 suite de l'attente vers un autre thread qui n'est pas bloqué, par exemple use var data = Task.Run(GetDataAsync).Result, qui affichera la suite dans le contexte de synchronisation d'un thread de 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().

Phillip Ngan
la source
En ce qui concerne, var data = Task.Run(GetDataAsync).Resultc'est nouveau pour moi. J'ai toujours pensé que l'extérieur .Resultserait facilement disponible dès que la première attente de GetDataAsyncest atteinte, il le datasera toujours default. Intéressant.
nawfal le
19

J'étais juste en train de jouer avec ce problème à nouveau dans un projet ASP.NET MVC. Lorsque vous souhaitez appeler des asyncméthodes à partir de a PartialView, vous n'êtes pas autorisé à créer le fichier 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 souhaitez appeler une asyncméthode à partir d' une méthode de synchronisation:

  1. Avant l'appel, effacez le SynchronizationContext
  2. Faites l'appel, il n'y aura plus de blocage ici, attendez qu'il 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);
}
Herre Kuijpers
la source
3

Un autre point principal est que vous ne devez pas bloquer sur les tâches, et utiliser async complètement pour éviter les blocages. Ensuite, ce sera 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();
}
marvelTracker
la source
6
Que faire si je souhaite que le thread principal (UI) soit bloqué jusqu'à la fin de la tâche? Ou dans une application console par exemple? Disons que je veux utiliser HttpClient, qui ne supporte que l'async ... Comment l'utiliser de manière synchrone sans risque de blocage ? Cela doit être possible. Si WebClient peut être utilisé de cette façon (en raison des méthodes de synchronisation) et fonctionne parfaitement, alors pourquoi ne pourrait-il pas être fait avec HttpClient également?
Dexter
Voir la réponse de Philip Ngan ci-dessus (je sais que cela a été posté après ce commentaire): Mettez en file d'attente la suite de l'attente vers un autre thread qui n'est pas bloqué, par exemple, utilisez var data = Task.Run (GetDataAsync) .Result
Jeroen
@Dexter - re " Que faire si je veux que le thread principal (UI) soit bloqué jusqu'à la fin de la tâche? " - voulez-vous vraiment que le thread UI soit bloqué, ce qui signifie que l'utilisateur ne peut rien faire, ne peut même pas annuler - ou est est-ce que vous ne voulez pas continuer la méthode dans laquelle vous êtes? "Wait" ou "Task.ContinueWith" traitent ce dernier cas.
ToolmakerSteve
@ToolmakerSteve bien sûr, je ne veux pas continuer la méthode. Mais je ne peux tout simplement pas utiliser await car je ne peux pas non plus utiliser async complètement - HttpClient est invoqué dans main , qui bien sûr ne peut pas être asynchrone. Et puis j'ai mentionné faire tout cela dans une application console - dans ce cas, je veux exactement la première - je ne veux même pas que mon application soit multithread. Bloquez tout .
Dexter
-1

Une solution à laquelle je suis arrivé est d'utiliser une Joinmé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ù est la méthode de jointure:

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)

Orace
la source