attendre vs Task.Wait - Deadlock?

194

Je ne comprends pas très bien la différence entre Task.Waitet await.

J'ai quelque chose de similaire aux fonctions suivantes dans un service ASP.NET WebAPI:

public class TestController : ApiController
{
    public static async Task<string> Foo()
    {
        await Task.Delay(1).ConfigureAwait(false);
        return "";
    }

    public async static Task<string> Bar()
    {
        return await Foo();
    }

    public async static Task<string> Ros()
    {
        return await Bar();
    }

    // GET api/test
    public IEnumerable<string> Get()
    {
        Task.WaitAll(Enumerable.Range(0, 10).Select(x => Ros()).ToArray());

        return new string[] { "value1", "value2" }; // This will never execute
    }
}

Getsera l'impasse.

Qu'est-ce qui pourrait provoquer cela? Pourquoi cela ne pose-t-il pas un problème lorsque j'utilise une attente de blocage plutôt que await Task.Delay?

ronag
la source
@Servy: Je reviendrai avec un repo dès que j'aurai le temps. Pour l'instant, cela fonctionne avec Task.Delay(1).Wait()ce qui est assez bon.
ronag
2
Task.Delay(1).Wait()est fondamentalement la même chose que Thread.Sleep(1000). Dans le code de production réel, il est rarement approprié.
Servy
@ronag: Votre WaitAllest à l'origine de l'impasse. Voir le lien vers mon blog dans ma réponse pour plus de détails. Vous devez utiliser à la await Task.WhenAllplace.
Stephen Cleary
6
@ronag Parce que vous avez ConfigureAwait(false)un seul appel àBarRos impasse ou ne la bloquerez pas, mais parce que vous avez un énumérable qui en crée plus d'une et attend ensuite toutes, la première barre bloquera la seconde. Si vous await Task.WhenAllau lieu d'attendre toutes les tâches, afin de ne pas bloquer le contexte ASP, vous verrez la méthode retourner normalement.
Servy
2
@ronag Votre autre option serait d'ajouter .ConfigureAwait(false) tout le long de l'arborescence jusqu'à ce que vous bloquiez, de cette façon, rien n'essaye jamais de revenir au contexte principal; Ça marcherait. Une autre option serait de faire tourner un contexte de synchronisation interne. Lien . Si vous mettez le Task.WhenAlldans un, AsyncPump.Runil bloquera efficacement le tout sans que vous ayez besoin de ConfigureAwaitquelque part, mais c'est probablement une solution trop complexe.
Servy

Réponses:

268

Waitet await- bien que similaires sur le plan conceptuel - sont en fait complètement différents.

Waitbloquera de façon synchrone jusqu'à la fin de la tâche. Ainsi, le thread actuel est littéralement bloqué en attendant la fin de la tâche. En règle générale, vous devez utiliser " asynctout en bas"; c'est-à-dire, ne bloquez pas le asynccode. Sur mon blog, j'entre dans les détails de la façon dont le blocage en code asynchrone provoque un blocage .

awaitattendra de manière asynchrone jusqu'à la fin de la tâche. Cela signifie que la méthode actuelle est "en pause" (son état est capturé) et la méthode renvoie une tâche incomplète à son appelant. Plus tard, une fois l' awaitexpression terminée, le reste de la méthode est planifié en tant que continuation.

Vous avez également mentionné un "bloc coopératif", par lequel je suppose que vous voulez dire qu'une tâche sur laquelle vous travaillez Waitpeut s'exécuter sur le thread en attente. Il y a des situations où cela peut arriver, mais c'est une optimisation. Il y a de nombreuses situations où cela ne peut pas se produire, comme si la tâche est pour un autre planificateur, ou si elle est déjà démarrée ou si c'est une tâche non codée (comme dans votre exemple de code: Waitne peut pas exécuter la Delaytâche en ligne car il n'y a pas de code pour ça).

Vous pouvez trouver mon async/ awaitintro utile.

Stephen Cleary
la source
4
Je pense qu'il y a un malentendu, Waitfonctionne bien les awaitblocages.
ronag
5
Non, le planificateur de tâches ne le fera pas. Waitbloque le thread, et il ne peut pas être utilisé pour d'autres choses.
Stephen Cleary
8
@ronag Je suppose que vous venez de mélanger les noms de vos méthodes et que votre blocage a été causé par le code de blocage et que vous avez travaillé avec le awaitcode. Soit cela, soit le blocage était sans rapport avec l'un et l'autre et vous avez mal diagnostiqué le problème.
Servy
3
@hexterminator: C'est par conception - cela fonctionne très bien pour les applications d'interface utilisateur, mais a tendance à gêner les applications ASP.NET. ASP.NET Core a résolu ce problème en supprimant le SynchronizationContext, de sorte que le blocage dans une demande ASP.NET Core ne provoque plus de blocage.
Stephen Cleary
1
sur msdn ici, il indique que l'attente s'exécute de manière asynchrone sur un thread séparé. Suis-je en train de manquer quelque chose? msdn.microsoft.com/en-us/library/hh195051(v=vs.110).aspx
batmaci
6

Sur la base de ce que j'ai lu à partir de différentes sources:

Une awaitexpression ne bloque pas le thread sur lequel elle s'exécute. Au lieu de cela, il oblige le compilateur à inscrire le reste de la asyncméthode en tant que continuation de la tâche attendue. Le contrôle revient ensuite à l'appelant de la asyncméthode. Une fois la tâche terminée, il invoque sa poursuite et l'exécution duasync méthode reprend là où elle s'était arrêtée.

Pour attendre la fin d'un single task, vous pouvez appeler sa Task.Waitméthode. Un appel à la Waitméthode bloque le thread appelant jusqu'à ce que l'instance de classe unique ait terminé l'exécution. La Wait()méthode sans paramètre est utilisée pour attendre sans condition la fin d'une tâche. La tâche simule le travail en appelant la Thread.Sleepméthode en veille pendant deux secondes.

Cet article est également une bonne lecture.

Ayushmati
la source
3
"N'est-ce pas techniquement incorrect alors? Quelqu'un peut-il clarifier?" - puis-je clarifier; posez-vous cela comme une question? (Je veux juste être clair si vous demandez vs répondez). Si vous posez la question: cela pourrait mieux fonctionner en tant que question distincte; il est peu probable de rassembler de nouvelles réponses ici comme réponse
Marc Gravell
1
J'ai répondu à la question et posé une question distincte pour le doute que j'avais ici stackoverflow.com/questions/53654006/… Merci @MarcGravell. Pouvez-vous s'il vous plaît supprimer votre vote de suppression pour la réponse maintenant?
Ayushmati
"Pouvez-vous s'il vous plaît supprimer votre vote de suppression pour la réponse maintenant?" - ce n'est pas le mien; grâce au ♦, un tel vote de ma part aurait pris effet immédiatement. Je ne pense pas, cependant, que cela réponde aux points clés de la question, qui concerne le comportement de blocage.
Marc Gravell
-2

Certains faits importants n'ont pas été mentionnés dans d'autres réponses:

"async wait" est plus complexe au niveau CIL et coûte donc de la mémoire et du temps CPU.

Toute tâche peut être annulée si le temps d'attente est inacceptable.

Dans le cas où "async wait" nous n'avons pas de gestionnaire pour une telle tâche pour l'annuler ou la surveiller.

L'utilisation de Task est plus flexible que "async wait".

Toute fonctionnalité de synchronisation peut être encapsulée par async.

public async Task<ActionResult> DoAsync(long id) 
{ 
    return await Task.Run(() => { return DoSync(id); } ); 
} 

Je ne vois pas pourquoi je dois vivre avec la méthode de synchronisation et async de duplication de code ou en utilisant des hacks.

user1785960
la source