Meilleure pratique pour appeler ConfigureAwait pour tout le code côté serveur

561

Lorsque vous avez du code côté serveur (c'est-à-dire certains ApiController) et que vos fonctions sont asynchrones - donc elles reviennent Task<SomeObject>- est-il considéré comme une meilleure pratique que chaque fois que vous attendez des fonctions que vous appelez ConfigureAwait(false)?

J'avais lu qu'il est plus performant car il n'a pas à basculer les contextes de thread vers le contexte de thread d'origine. Cependant, avec ASP.NET Web Api, si votre demande arrive sur un thread et que vous attendez une fonction et un appel ConfigureAwait(false)qui pourraient potentiellement vous mettre sur un thread différent lorsque vous renvoyez le résultat final de votre ApiControllerfonction.

J'ai tapé un exemple de ce dont je parle ci-dessous:

public class CustomerController : ApiController
{
    public async Task<Customer> Get(int id)
    {
        // you are on a particular thread here
        var customer = await SomeAsyncFunctionThatGetsCustomer(id).ConfigureAwait(false);

        // now you are on a different thread!  will that cause problems?
        return customer;
    }
}
Arash Emami
la source

Réponses:

628

Mise à jour: ASP.NET Core n'a pas deSynchronizationContext . Si vous êtes sur ASP.NET Core, peu importe que vous l'utilisiez ConfigureAwait(false)ou non.

Pour ASP.NET "Full" ou "Classic" ou autre, le reste de cette réponse s'applique toujours.

Message d'origine (pour ASP.NET non-Core):

Cette vidéo de l'équipe ASP.NET contient les meilleures informations sur l'utilisation asyncsur ASP.NET.

J'avais lu qu'il est plus performant car il n'a pas à basculer les contextes de thread vers le contexte de thread d'origine.

Cela est vrai avec les applications d'interface utilisateur, où il n'y a qu'un seul thread d'interface utilisateur sur lequel vous devez "synchroniser".

Dans ASP.NET, la situation est un peu plus complexe. Lorsqu'une asyncméthode reprend l'exécution, elle récupère un thread du pool de threads ASP.NET. Si vous désactivez la capture de contexte à l'aide ConfigureAwait(false), le thread continue simplement d'exécuter la méthode directement. Si vous ne désactivez pas la capture de contexte, le thread ré-entrera dans le contexte de la demande et continuera ensuite à exécuter la méthode.

Donc, ConfigureAwait(false)ne vous enregistre pas un saut de fil dans ASP.NET; cela vous évite de rentrer dans le contexte de la demande, mais cela est normalement très rapide. ConfigureAwait(false) pourrait être utile si vous essayez de faire une petite quantité de traitement parallèle d'une demande, mais en réalité, TPL est mieux adapté à la plupart de ces scénarios.

Cependant, avec ASP.NET Web Api, si votre demande arrive sur un thread et que vous attendez une fonction et appelez ConfigureAwait (false) qui pourrait potentiellement vous mettre sur un autre thread lorsque vous renvoyez le résultat final de votre fonction ApiController .

En fait, juste faire un awaitpeut le faire. Une fois que votre asyncméthode atteint un await, la méthode est bloquée mais le thread retourne au pool de threads. Lorsque la méthode est prête à continuer, tout thread est extrait du pool de threads et utilisé pour reprendre la méthode.

La seule différence ConfigureAwaitdans ASP.NET est de savoir si ce thread entre dans le contexte de la demande lors de la reprise de la méthode.

J'ai plus d'informations de fond dans mon article MSDN surSynchronizationContext et mon asyncblog d'introduction .

Stephen Cleary
la source
23
Le stockage thread-local n'est géré par aucun contexte. HttpContext.Currentest acheminé par ASP.NET SynchronizationContext, qui est acheminé par défaut lorsque vous await, mais il n'est pas acheminé par ContinueWith. OTOH, le contexte d'exécution (y compris les restrictions de sécurité) est le contexte mentionné dans CLR via C #, et il est transmis par les deux ContinueWithet await(même si vous utilisez ConfigureAwait(false)).
Stephen Cleary
65
Ne serait-ce pas génial si C # avait le support du langage natif pour ConfigureAwait (false)? Quelque chose comme 'waitnc' (n'attendez aucun contexte). Taper un appel de méthode séparé partout est assez ennuyeux. :)
NathanAldenSr
19
@NathanAldenSr: On en a beaucoup discuté. Le problème avec un nouveau mot-clé est que cela ConfigureAwaitn'a de sens que lorsque vous attendez des tâches , alors qu'il awaitagit sur tout "en attente". Les autres options envisagées étaient les suivantes: le comportement par défaut doit-il ignorer le contexte s'il se trouve dans une bibliothèque? Ou avez-vous un paramètre de compilation pour le comportement de contexte par défaut? Les deux ont été rejetés car il est plus difficile de simplement lire le code et de dire ce qu'il fait.
Stephen Cleary
10
@AnshulNigam: C'est pourquoi les actions du contrôleur ont besoin de leur contexte. Mais la plupart des méthodes que les actions appellent ne le font pas.
Stephen Cleary
14
@JonathanRoeder: De manière générale, vous ne devriez pas avoir besoin ConfigureAwait(false)d'éviter un blocage basé sur Result/ Waitcar sur ASP.NET, vous ne devriez pas utiliser Result/ Waiten premier lieu.
Stephen Cleary
131

Réponse brève à votre question: Non. Vous ne devriez pas appeler ConfigureAwait(false)au niveau de l'application comme ça.

Version TL; DR de la réponse longue: Si vous écrivez une bibliothèque où vous ne connaissez pas votre consommateur et n'avez pas besoin d'un contexte de synchronisation (ce que vous ne devriez pas dans une bibliothèque, je crois), vous devez toujours utiliser ConfigureAwait(false). Sinon, les consommateurs de votre bibliothèque peuvent faire face à des blocages en consommant vos méthodes asynchrones de manière bloquante. Cela dépend de la situation.

Voici une explication un peu plus détaillée sur l'importance de la ConfigureAwaitméthode (une citation de mon article de blog):

Lorsque vous attendez une méthode avec le mot-clé wait, le compilateur génère un tas de code en votre nom. L'un des objectifs de cette action est de gérer la synchronisation avec le thread d'interface utilisateur (ou principal). Le composant clé de cette fonctionnalité est celui SynchronizationContext.Currentqui obtient le contexte de synchronisation pour le thread actuel. SynchronizationContext.Currentest rempli en fonction de l'environnement dans lequel vous vous trouvez. La GetAwaiterméthode de tâche recherche SynchronizationContext.Current. Si le contexte de synchronisation actuel n'est pas nul, la continuation qui est transmise à ce serveur sera renvoyée dans ce contexte de synchronisation.

Lorsque vous consommez une méthode, qui utilise les nouvelles fonctionnalités du langage asynchrone, de manière bloquante, vous vous retrouverez avec un blocage si vous avez un SynchronizationContext disponible. Lorsque vous consommez de telles méthodes de manière bloquante (en attente de la méthode Task with Wait ou en prenant le résultat directement à partir de la propriété Result de la tâche), vous bloquerez le thread principal en même temps. Lorsque finalement la tâche se termine à l'intérieur de cette méthode dans le pool de threads, elle va invoquer la continuation pour publier sur le thread principal car elle SynchronizationContext.Currentest disponible et capturée. Mais il y a un problème ici: le thread de l'interface utilisateur est bloqué et vous avez un blocage!

En outre, voici deux excellents articles pour vous qui sont exactement pour votre question:

Enfin, il y a une excellente courte vidéo de Lucian Wischik sur ce sujet: les méthodes de bibliothèque asynchrone devraient envisager d'utiliser Task.ConfigureAwait (false) .

J'espère que cela t'aides.

tugberk
la source
2
"La méthode GetAwaiter de Task recherche SynchronizationContext.Current. Si le contexte de synchronisation actuel n'est pas nul, la suite qui est transmise à cet serveur d'attente sera publiée dans ce contexte de synchronisation." - J'ai l'impression que vous essayez de dire que Taskmarche la pile pour obtenir le SynchronizationContext, ce qui est faux. Le SynchronizationContextest saisi avant l'appel à la Task, puis le reste du code se poursuit sur le SynchronizationContextif SynchronizationContext.Currentn'est pas nul.
casperOne
1
@casperOne J'ai l'intention de dire la même chose.
tugberk
8
Ne devrait-il pas être de la responsabilité de l'appelant de s'assurer que SynchronizationContext.Currentc'est clair / ou que la bibliothèque est appelée dans un Task.Run()au lieu d'avoir à écrire .ConfigureAwait(false)partout dans la bibliothèque de classe?
binki
1
@binki - d'un autre côté: (1) une bibliothèque est probablement utilisée dans de nombreuses applications, donc faire des efforts une seule fois dans la bibliothèque pour faciliter les applications est rentable; (2) l'auteur de la bibliothèque sait vraisemblablement qu'il a écrit du code qui n'a aucune raison d'exiger de continuer sur le contexte d'origine, qu'il exprime par ces .ConfigureAwait(false)art. Il serait peut-être plus facile pour les auteurs de bibliothèques si tel était le comportement par défaut, mais je présume qu'il est un peu plus difficile d'écrire une bibliothèque correctement que de rendre un peu plus difficile l'écriture d'une application correctement.
ToolmakerSteve
4
Pourquoi l'auteur d'une bibliothèque devrait-il dorloter le consommateur? Si le consommateur souhaite un blocage, pourquoi devrais-je l'empêcher?
Quarkly
25

Le plus grand inconvénient que j'ai trouvé avec l'utilisation de ConfigureAwait (false) est que la culture de thread est rétablie par défaut. Si vous avez configuré une culture, par exemple ...

<system.web>
    <globalization culture="en-AU" uiCulture="en-AU" />    
    ...

et que vous hébergez sur un serveur dont la culture est définie sur en-US, vous trouverez avant que ConfigureAwait (false) ne s'appelle CultureInfo.CurrentCulture renverra en-AU et après que vous obtiendrez en-US. c'est à dire

// CultureInfo.CurrentCulture ~ {en-AU}
await xxxx.ConfigureAwait(false);
// CultureInfo.CurrentCulture ~ {en-US}

Si votre application fait quelque chose qui nécessite un formatage de données spécifique à la culture, vous devrez en être conscient lors de l'utilisation de ConfigureAwait (false).

Mick
la source
27
Les versions modernes de .NET (je pense que depuis 4.6?) Propageront la culture à travers les threads, même si elle ConfigureAwait(false)est utilisée.
Stephen Cleary
1
Merci pour l'info. Nous utilisons en effet .net 4.5.2
Mick
11

J'ai quelques réflexions générales sur la mise en œuvre de Task:

  1. La tâche est jetable mais nous ne sommes pas censés l' utiliser using.
  2. ConfigureAwaita été introduit en 4.5. Taska été introduit en 4.0.
  3. Les threads .NET sont toujours utilisés pour faire circuler le contexte (voir C # via le livre CLR), mais dans l'implémentation par défaut, Task.ContinueWithils ne sont pas b / c, il a été réalisé que le changement de contexte est coûteux et il est désactivé par défaut.
  4. Le problème est qu'un développeur de bibliothèque ne devrait pas se soucier de savoir si ses clients ont besoin d'un flux de contexte ou non, il ne doit donc pas décider si le flux doit être déplacé ou non.
  5. [Ajouté plus tard] Le fait qu'il n'y ait pas de réponse faisant autorité et de référence appropriée et que nous continuons à nous battre sur cela signifie que quelqu'un n'a pas fait son travail correctement.

J'ai quelques articles sur le sujet, mais mon point de vue - en plus de la belle réponse de Tugberk - est que vous devez activer toutes les API asynchrones et idéalement gérer le contexte. Puisque vous effectuez une async, vous pouvez simplement utiliser des continuations au lieu d'attendre, donc aucun blocage ne sera causé car aucune attente n'est effectuée dans la bibliothèque et vous gardez le flux afin que le contexte soit préservé (tel que HttpContext).

Le problème est lorsqu'une bibliothèque expose une API synchrone mais utilise une autre API asynchrone - vous devez donc utiliser Wait()/ Resultdans votre code.

Aliostad
la source
6
1) Vous pouvez appeler Task.Disposesi vous le souhaitez; vous n'avez tout simplement pas besoin de la grande majorité du temps. 2) a Taskété introduit dans .NET 4.0 dans le cadre du TPL, ce qui n'était pas nécessaire ConfigureAwait; une fois asyncajouté, ils ont réutilisé le Tasktype existant au lieu d'en inventer un nouveau Future.
Stephen Cleary
6
3) Vous confondez deux types différents de «contexte». Le "contexte" mentionné en C # via CLR est toujours transmis, même en Tasks; le "contexte" contrôlé par ContinueWithest un SynchronizationContextou TaskScheduler. Ces différents contextes sont expliqués en détail sur le blog de Stephen Toub .
Stephen Cleary
21
4) L'auteur de la bibliothèque n'a pas besoin de se soucier de savoir si ses appelants ont besoin du flux de contexte, car chaque méthode asynchrone reprend indépendamment. Donc, si les appelants ont besoin du flux de contexte, ils peuvent le faire circuler, que l'auteur de la bibliothèque le fasse ou non.
Stephen Cleary
1
Au début, vous semblez vous plaindre au lieu de répondre à la question. Et puis vous parlez du «contexte», sauf qu'il existe plusieurs types de contexte dans .Net et il n'est vraiment pas clair de quel (ou de ceux?) Dont vous parlez. Et même si vous n'êtes pas confus vous-même (mais je pense que vous l'êtes, je crois qu'il n'y a pas de contexte qui coulait avec Threads, mais plus avec ContinueWith()), cela rend votre réponse confuse à lire.
svick
1
@StephenCleary oui, le développeur de lib ne devrait pas avoir besoin de le savoir, c'est au client. Je pensais que c'était clair, mais ma formulation n'était pas claire.
Aliostad