ConfigureAwait: Foire aux questions

Bonjour, Habr! Je vous présente la traduction de l' article FAQ ConfigureAwait de Stephen Taub.

image

Async / await ajouté à .NET il y a plus de sept ans. Cette décision a eu un impact significatif non seulement sur l'écosystème .NET - elle se reflète également dans de nombreux autres langages et cadres. Actuellement, de nombreuses améliorations dans .NET ont été mises en œuvre en termes de constructions de langage supplémentaires utilisant asynchronie, des API avec prise en charge asynchrone ont été mises en œuvre, des améliorations fondamentales ont été apportées à l'infrastructure grâce à laquelle async / await fonctionne comme une horloge (en particulier, les performances et les capacités de diagnostic ont été améliorées dans .NET Core).

ConfigureAwait est un aspect de l' async / await qui continue de soulever des questions. J'espère pouvoir répondre à bon nombre d'entre eux. Je vais essayer de rendre cet article lisible du début à la fin, et en même temps de l'exécuter dans le style des réponses aux questions fréquemment posées (FAQ) afin qu'il puisse être référencé à l'avenir.

Pour traiter réellement avec ConfigureAwait , nous y reviendrons un peu.

Qu'est-ce qu'un SynchronizationContext?


Selon la documentation System.Threading.SynchronizationContext "Fournit des fonctionnalités de base pour distribuer le contexte de synchronisation dans divers modèles de synchronisation." Cette définition n'est pas tout à fait évidente.

Dans 99,9% des cas, le SynchronizationContext utilisé simplement comme un type avec une méthode Post virtuelle qui accepte un délégué pour une exécution asynchrone (il existe d'autres membres virtuels dans le SynchronizationContext , mais ils sont moins courants et ne seront pas abordés dans cet article). La méthode Post du type de base appelle littéralement simplement ThreadPool.QueueUserWorkItem pour exécuter de manière asynchrone le délégué fourni. Les types dérivés remplacent Post afin que le délégué puisse s'exécuter au bon endroit au bon moment.

Par exemple, Windows Forms a un type dérivé de SynchronizationContext qui redéfinit Post pour faire l'équivalent de Control.BeginInvoke . Cela signifie que tout appel à cette méthode Post entraînera un appel au délégué à un stade ultérieur du thread associé au contrôle correspondant - le soi-disant thread d'interface utilisateur. Au cœur de Windows Forms se trouve le traitement des messages Win32. La boucle de message est exécutée dans un thread d'interface utilisateur qui n'attend que le traitement des nouveaux messages. Ces messages sont déclenchés par le mouvement de la souris, les clics, la saisie au clavier, les événements système qui sont disponibles pour être exécutés par les délégués, etc. Par conséquent, si vous avez une instance SynchronizationContext pour un thread d'interface utilisateur dans une application Windows Forms, vous devez transmettre le délégué à la méthode Post afin d'y effectuer une opération.

Windows Presentation Foundation (WPF) possède également un type dérivé de SynchronizationContext avec une méthode Post remplacée qui "dirige" de la même manière le délégué vers le flux d'interface utilisateur (à l'aide de Dispatcher.BeginInvoke ), avec le contrôle WPF Dispatcher, pas le contrôle Windows Forms.

Et Windows RunTime (WinRT) a son propre type dérivé de SynchronizationContext , qui place également le délégué dans la CoreDispatcher threads de l'interface utilisateur à l'aide de CoreDispatcher .

C'est ce qui se cache derrière l'expression «exécuter le délégué dans le thread d'interface utilisateur». Vous pouvez également implémenter votre SynchronizationContext avec la méthode Post et une certaine implémentation. Par exemple, je n'ai pas à me soucier du thread dans lequel s'exécute le délégué, mais je veux être sûr que tous Post délégués de la méthode Post dans mon SynchronizationContext s'exécutent avec un certain degré de parallélisme limité. Vous pouvez implémenter un SynchronizationContext personnalisé de cette façon:

 internal sealed class MaxConcurrencySynchronizationContext : SynchronizationContext { private readonly SemaphoreSlim _semaphore; public MaxConcurrencySynchronizationContext(int maxConcurrencyLevel) => _semaphore = new SemaphoreSlim(maxConcurrencyLevel); public override void Post(SendOrPostCallback d, object state) => _semaphore.WaitAsync().ContinueWith(delegate { try { d(state); } finally { _semaphore.Release(); } }, default, TaskContinuationOptions.None, TaskScheduler.Default); public override void Send(SendOrPostCallback d, object state) { _semaphore.Wait(); try { d(state); } finally { _semaphore.Release(); } } } 

Le framework xUnit a une implémentation similaire de SynchronizationContext. Ici, il est utilisé pour réduire la quantité de code associée aux tests parallèles.

Les avantages ici sont les mêmes que pour toute abstraction: une seule API est fournie qui peut être utilisée pour mettre en file d'attente le délégué pour l'exécution de la manière souhaitée par le programmeur, sans avoir à connaître les détails de l'implémentation. Supposons que j'écrive une bibliothèque où j'ai besoin de faire un peu de travail, puis que j'attende un délégué dans le contexte d'origine. Pour ce faire, j'ai besoin de capturer son SynchronizationContext , et lorsque j'aurai terminé ce qui est nécessaire, je n'aurai qu'à appeler la méthode Post de ce contexte et lui passer un délégué pour l'exécution. Je n'ai pas besoin de savoir que pour Windows Forms, vous devez prendre le Control et utiliser son BeginInvoke , pour WPF utiliser BeginInvoke de Dispatcher , ou obtenir le contexte et sa file d'attente pour xUnit. Tout ce que je dois faire est de saisir le SynchronizationContext actuel et de l'utiliser plus tard. Pour ce faire, le SynchronizationContext a une propriété Current . Cela peut être implémenté comme suit:

 public void DoWork(Action worker, Action completion) { SynchronizationContext sc = SynchronizationContext.Current; ThreadPool.QueueUserWorkItem(_ => { try { worker(); } finally { sc.Post(_ => completion(), null); } }); } 

Vous pouvez définir un contexte spécial à partir de la propriété Current à l'aide de la méthode SynchronizationContext.SetSynchronizationContext .

Qu'est-ce qu'un planificateur de tâches?


SynchronizationContext est une abstraction courante pour le «planificateur». Certains frameworks utilisent leurs propres abstractions pour cela, et System.Threading.Tasks ne System.Threading.Tasks pas exception. Lorsqu'il existe des délégués dans la Task qui peuvent être mis en file d'attente et exécutés, ils sont associés à System.Threading.Tasks.TaskScheduler . Il existe également une méthode Post virtuelle pour mettre en file d'attente un délégué (un appel de délégué est implémenté à l'aide de mécanismes standard), TaskScheduler fournit une méthode QueueTask abstraite (un appel de tâche est implémenté à l'aide de la méthode ExecuteTask ).

Le planificateur par défaut qui renvoie TaskScheduler.Default est un pool de threads. À partir de TaskScheduler il est également possible d'obtenir et de remplacer des méthodes pour définir l'heure et le lieu de l'appel de Task . Par exemple, les bibliothèques principales incluent le type System.Threading.Tasks.ConcurrentExclusiveSchedulerPair . Une instance de cette classe fournit deux propriétés TaskScheduler : ExclusiveScheduler et ConcurrentScheduler . Les tâches planifiées dans ConcurrentScheduler peuvent être exécutées en parallèle, mais en tenant compte de la restriction définie par ConcurrentExclusiveSchedulerPair lors de sa création (similaire à MaxConcurrencySynchronizationContext ). Aucune tâche ConcurrentScheduler ne sera exécutée si la tâche est exécutée dans ExclusiveScheduler et qu'une seule tâche exclusive est autorisée à s'exécuter à la fois. Ce comportement est très similaire à un verrou en lecture / écriture.

Comme SynchronizationContext , TaskScheduler a une propriété Current qui renvoie le TaskScheduler actuel. Cependant, contrairement à SynchronizationContext , il manque une méthode pour définir le planificateur actuel. Au lieu de cela, le planificateur est associé à la tâche en cours. Ainsi, par exemple, ce programme affichera True , car le lambda utilisé dans StartNew est exécuté dans l'instance ExclusiveScheduler de ConcurrentExclusiveSchedulerPair , et TaskScheduler.Current installé sur ce planificateur:

 using System; using System.Threading.Tasks; class Program { static void Main() { var cesp = new ConcurrentExclusiveSchedulerPair(); Task.Factory.StartNew(() => { Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler); }, default, TaskCreationOptions.None, cesp.ExclusiveScheduler).Wait(); } } 

Fait intéressant, TaskScheduler fournit une méthode FromCurrentSynchronizationContext statique. La méthode crée un nouveau TaskScheduler et met en TaskScheduler les tâches à exécuter dans le contexte SynchronizationContext.Current renvoyé à l'aide de la méthode Post .

Comment le SynchronizationContext et le TaskScheduler sont-ils liés à l'attente?


Disons que vous devez écrire une application d'interface utilisateur avec un bouton. Une pression sur le bouton lance le téléchargement du texte du site Web et le définit sur le bouton Content . Le bouton ne doit être accessible qu'à partir de l'interface utilisateur du flux dans lequel il se trouve.Par conséquent, lorsque nous chargeons avec succès la date et l'heure et que nous voulons les placer dans le Content du bouton, nous devons le faire à partir du flux qui le contrôle. Si cette condition n'est pas remplie, nous obtiendrons une exception:

 System.InvalidOperationException: '        ,     .' 

Nous pouvons utiliser manuellement le SynchronizationContext pour définir le Content dans le contexte source, par exemple via TaskScheduler :

 private static readonly HttpClient s_httpClient = new HttpClient(); private void downloadBtn_Click(object sender, RoutedEventArgs e) { s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask => { downloadBtn.Content = downloadTask.Result; }, TaskScheduler.FromCurrentSynchronizationContext()); } 

Et nous pouvons utiliser le SynchronizationContext directement:

 private static readonly HttpClient s_httpClient = new HttpClient(); private void downloadBtn_Click(object sender, RoutedEventArgs e) { SynchronizationContext sc = SynchronizationContext.Current; s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask => { sc.Post(delegate { downloadBtn.Content = downloadTask.Result; }, null); }); } 

Cependant, ces deux options utilisent explicitement un rappel. Au lieu de cela, nous pouvons utiliser async / await :

 private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime"); downloadBtn.Content = text; } 

Tout cela «fonctionne tout simplement» et configure avec succès le Content dans le thread d'interface utilisateur, car dans le cas de la version implémentée manuellement ci-dessus, par défaut, l'attente d'une tâche fait référence à SynchronizationContext.Current et TaskScheduler.Current . Lorsque vous «attendez» quelque chose en C #, le compilateur convertit le code d'interrogation (en appelant GetAwaiter ) le «attendu» (dans ce cas, la tâche) en «en attente» ( TaskAwaiter ). L '«attente» est responsable de l'attachement d'un rappel (souvent appelé «continuation») qui rappelle à la machine d'état lorsque l'attente est terminée. Il implémente cela en utilisant le contexte / planificateur qu'il a capturé lors de l'enregistrement du rappel. Nous allons optimiser et configurer un peu, c'est quelque chose comme ceci:

 object scheduler = SynchronizationContext.Current; if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default) { scheduler = TaskScheduler.Current; } 

Ici, il est d'abord vérifié si le SynchronizationContext , et sinon, s'il TaskScheduler non standard. S'il y en a un, alors lorsque le rappel est prêt pour l'appel, l'ordonnanceur capturé sera utilisé; sinon, le rappel sera exécuté dans le cadre de l'opération qui termine la tâche attendue.

Que fait ConfigureAwait (faux)


La méthode ConfigureAwait n'est pas spéciale: elle n'est reconnue d'aucune manière particulière par le compilateur ou le runtime. Il s'agit d'une méthode normale qui renvoie une structure ( ConfiguredTaskAwaitable - encapsule la tâche d'origine) et prend une valeur booléenne. N'oubliez pas que l' await peut être utilisée avec n'importe quel type qui implémente le modèle correct. Si un autre type est renvoyé, cela signifie que lorsque le compilateur obtient l'accès à la méthode GetAwaiter (partie du modèle) des instances, mais le fait à partir du type renvoyé par ConfigureAwait , et non directement à partir de la tâche. Cela vous permet de modifier le comportement d' await de ce serveur spécial.

Attendre le type renvoyé par ConfigureAwait(continueOnCapturedContext: false) au lieu d'attendre la Task affecte directement l'implémentation de capture de contexte / planificateur décrite ci-dessus. La logique devient quelque chose comme ceci:

 object scheduler = null; if (continueOnCapturedContext) { scheduler = SynchronizationContext.Current; if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default) { scheduler = TaskScheduler.Current; } } 

En d'autres termes, la spécification de false , même s'il existe un contexte ou un planificateur actuel pour le rappel, implique qu'il est absent.

Pourquoi dois-je utiliser ConfigureAwait (false)?


ConfigureAwait(continueOnCapturedContext: false) utilisé pour empêcher le rappel d'être forcé d'appeler dans le contexte source ou le planificateur. Cela nous donne plusieurs avantages:

Amélioration des performances. Il y a une surcharge de mise en file d'attente d'un rappel, contrairement à un simple appel, car cela nécessite un travail supplémentaire (et généralement une allocation supplémentaire). De plus, nous ne pouvons pas utiliser l'optimisation lors de l'exécution (nous pouvons optimiser davantage lorsque nous savons exactement comment le rappel sera appelé, mais s'il est transmis à une implémentation arbitraire d'abstraction, cela impose parfois des restrictions). Pour les sections fortement chargées, même les coûts supplémentaires liés à la vérification du SynchronizationContext actuel et du TaskScheduler actuel (qui impliquent tous deux l'accès aux flux statiques) peuvent augmenter considérablement les frais généraux. Si le code après await ne nécessite pas d'exécution dans le contexte d'origine, à l'aide de ConfigureAwait(false) , toutes ces dépenses peuvent être évitées, car il n'a pas besoin d'être mis en file d'attente inutilement, il peut utiliser toutes les optimisations disponibles et peut également éviter un accès inutile aux statiques du flux.

Prévention de l'impasse. Considérez la méthode de bibliothèque qui await utilisations pour télécharger quelque chose à partir du réseau. Vous appelez cette méthode et bloquez de manière synchrone, en attendant que la tâche se termine, par exemple, en utilisant .Wait() ou .Result ou .GetAwaiter() .GetResult() . Considérez maintenant ce qui se passe si l'appel se produit lorsque le SynchronizationContext actuel limite le nombre d'opérations qu'il MaxConcurrencySynchronizationContext à 1 en utilisant explicitement MaxConcurrencySynchronizationContext , ou implicitement, s'il s'agit d'un contexte avec un seul thread à utiliser (par exemple, un thread d'interface utilisateur). Ainsi, vous appelez la méthode dans un seul thread, puis la bloquez, en attendant que l'opération se termine. Le téléchargement démarre sur le réseau et attend sa fin. Par défaut, l'attente d'une Task capturera le SynchronizationContext (et dans ce cas), et lorsque le téléchargement à partir du réseau sera terminé, il sera mis en file d'attente vers le rappel SynchronizationContext , qui appellera le reste de l'opération. Mais le seul thread qui peut gérer le rappel dans la file d'attente est actuellement bloqué en attendant la fin de l'opération. Et cette opération ne sera pas terminée tant que le rappel n'aura pas été traité. Impasse! Cela peut se produire même lorsque le contexte ne limite pas la simultanéité à 1, mais que les ressources sont limitées d'une manière ou d'une autre. Imaginez la même situation, uniquement avec une valeur de 4 pour MaxConcurrencySynchronizationContext . Au lieu d'exécuter l'opération une fois, nous mettons en file d'attente 4 appels au contexte. Chaque appel est effectué et se verrouille en prévision de son achèvement. Toutes les ressources sont désormais bloquées en attendant la fin des méthodes asynchrones, et la seule chose qui leur permettra de se terminer est si leurs rappels sont traités par ce contexte. Cependant, il est déjà pleinement occupé. Impasse à nouveau. Si la méthode de bibliothèque utilisait ConfigureAwait(false) place, elle ne mettrait pas en file d'attente le rappel au contexte d'origine, ce qui éviterait les scripts de blocage.

Dois-je utiliser ConfigureAwait (vrai)?


Non, sauf si vous devez indiquer explicitement que vous n'utilisez pas ConfigureAwait(false) (par exemple, pour masquer les avertissements d'analyse statique, etc.). ConfigureAwait(true) ne fait rien de significatif. Si vous comparez la await task et la await task await task.ConfigureAwait(true) , ils seront fonctionnellement identiques. Ainsi, si ConfigureAwait(true) présent dans le code, il peut être supprimé sans aucune conséquence négative.

La méthode ConfigureAwait prend une valeur booléenne, car dans certaines situations, il peut être nécessaire de transmettre une variable pour contrôler la configuration. Mais dans 99% des cas, la valeur est définie sur false, ConfigureAwait(false) .

Quand utiliser ConfigureAwait (faux)?


Cela dépend de l'implémentation du code au niveau de l'application ou du code de bibliothèque à usage général.

Lors de l'écriture d'applications, un comportement par défaut est généralement requis. Si le modèle / environnement d'application (par exemple, Windows Forms, WPF, ASP.NET Core) publie un SynchronizationContext spécial, il y a presque certainement une bonne raison à cela: cela signifie que le code vous permet de prendre soin du contexte de synchronisation pour une interaction correcte avec le modèle / environnement d'application. Par exemple, si vous écrivez un gestionnaire d'événements dans une application Windows Forms, un test dans xUnit ou du code dans un contrôleur ASP.NET MVC, que le modèle d'application ait ou non publié un SynchronizationContext , vous devez utiliser SynchronizationContext s'il en existe un. Cela signifie que si ConfigureAwait(true) et await , les rappels / continuations sont renvoyés au contexte d'origine - tout se passe comme il se doit. De là, vous pouvez formuler une règle générale: si vous écrivez du code de niveau application, n'utilisez pas ConfigureAwait(false) . Revenons au gestionnaire de clics:

 private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime"); downloadBtn.Content = text; } 

downloadBtn.Content = text doit être exécuté dans le contexte d'origine. Si le code a violé cette règle et utilisé à la place ConfigureAwait (false) , il ne sera pas utilisé dans le contexte d'origine:

 private static readonly HttpClient s_httpClient = new HttpClient(); private async void downloadBtn_Click(object sender, RoutedEventArgs e) { string text = await s_httpClient.GetStringAsync("http://example.com/currenttime").ConfigureAwait(false); //  downloadBtn.Content = text; } 

cela conduira à un comportement inapproprié. La même chose s'applique au code dans une application ASP.NET classique qui dépend de HttpContext.Current . Lors de l'utilisation de ConfigureAwait(false) tentative ultérieure d'utilisation de la fonction Context.Current est susceptible de provoquer des problèmes.

C'est ce qui distingue les bibliothèques à usage général. Ils sont universels en partie parce qu'ils ne se soucient pas de l'environnement dans lequel ils sont utilisés. Vous pouvez les utiliser à partir d'une application Web, d'une application client ou d'un test - cela n'a pas d'importance, car le code de la bibliothèque est indépendant du modèle d'application dans lequel il peut être utilisé. Agnostic signifie également que la bibliothèque ne fera rien pour interagir avec le modèle d'application, par exemple, elle n'aura pas accès aux contrôles de l'interface utilisateur, car la bibliothèque à usage général n'en sait rien. Puisqu'il n'est pas nécessaire d'exécuter le code dans un environnement particulier, nous pouvons éviter de forcer les continuations / rappels à être forcés dans le contexte d'origine, et nous le faisons en utilisant ConfigureAwait(false) , ce qui nous donne des avantages de performance et augmente la fiabilité. Cela nous amène à ce qui suit: si vous écrivez du code de bibliothèque à usage général, utilisez ConfigureAwait(false) . C'est pourquoi chaque (ou presque) attente dans les bibliothèques d'exécution .NET Core utilise ConfigureAwait (false); À quelques exceptions près, qui sont probablement des bogues, ils seront corrigés.Par exemple, le PR corrigé aucun appel ConfigureAwait(false)en HttpClient.

Bien sûr, cela n'a pas de sens partout. Par exemple, l'une des grandes exceptions (ou du moins les cas où vous devez y penser) dans les bibliothèques à usage général est lorsque ces bibliothèques ont des API qui acceptent les délégués à un appel. Dans de tels cas, la bibliothèque accepte le code potentiel au niveau de l'application de l'appelant, ce qui rend ces hypothèses pour la bibliothèque à usage général très controversées. Imaginez, par exemple, la version asynchrone de la méthode Where LINQ:. public static async IAsyncEnumerable<T> WhereAsync(this IAsyncEnumerable<T> source, Func<T, bool> predicate)Doit-elle predicateêtre appelée dans la source du SynchronizationContextcode appelant? Cela dépend de l'implémentation WhereAsync, et c'est la raison pour laquelle il peut décider de ne pas l'utiliser ConfigureAwait(false).

Même dans des cas particuliers, suivez la recommandation générale: utilisez ConfigureAwait(false)si vous écrivez un code agnostique de bibliothèque / app-model-agnostic à usage général.

ConfigureAwait (false) garantit-il que le rappel ne sera pas exécuté dans le contexte d'origine?


Non, cela garantit qu'il ne sera pas mis en file d'attente dans le contexte d'origine. Mais cela ne signifie pas que le code suivant awaitne sera pas exécuté dans le contexte d'origine. Cela est dû au fait que les opérations déjà terminées sont renvoyées de manière synchrone et ne sont pas forcément renvoyées dans la file d'attente. Par conséquent, si vous attendez une tâche qui a déjà été terminée au moment où vous attendez, que vous l'utilisiez ou non ConfigureAwait(false), le code immédiatement après continuera à s'exécuter dans le thread en cours dans un contexte toujours valide.

ConfigureAwait (false) , — ?


En général, non. N'oubliez pas la FAQ précédente. S'il await task.ConfigureAwait(false)inclut une tâche qui a déjà été terminée au moment de l'attente (ce qui se produit en fait assez souvent), alors l'utilisation ConfigureAwait(false)sera inutile, car le thread continue d'exécuter le code suivant dans la méthode et est toujours dans le même contexte qu'auparavant.

Une exception notable est que la première awaitse terminera toujours de manière asynchrone et que l'opération attendue la rappellera dans un environnement exempt de spécial SynchronizationContextou TaskScheduler. Par exemple, CryptoStreamdans les bibliothèques d'exécution, .NET vérifie que son code potentiellement intensif en calcul n'est pas exécuté dans le cadre d'une invocation synchrone du code appelant. Pour ce faire, il utilise unawaiterpour vous assurer que le code après la première attente est exécuté dans le thread du pool de threads. Cependant, même dans ce cas, vous remarquerez que la prochaine attente utilise toujours ConfigureAwait(false); Techniquement, cela n'est pas nécessaire, mais cela simplifie considérablement la révision du code, car il n'est pas nécessaire de comprendre pourquoi il n'a pas été utilisé ConfigureAwait(false).

Est-il possible d'utiliser Task.Run pour Ă©viter d'utiliser ConfigureAwait (false)?


Oui, si vous Ă©crivez:

 Task.Run(async delegate { await SomethingAsync(); //     }); 

puis ConfigureAwait(false)en SomethingAsync()serait superflue, comme le délégué passé à Task.Runexécuter dans un pool de threads de flux, de sorte qu'aucune modification du code ci - dessus, SynchronizationContext.Currentrenvoie la valeur null. De plus, il Task.Runutilise implicitement TaskScheduler.Default, par conséquent, TaskScheduler.Currentà l'intérieur du délégué retournera également une valeur Default. Cela signifie qu'il awaitaura le même comportement, qu'il ait été utilisé ou non ConfigureAwait(false). Il ne peut pas non plus garantir ce que le code à l'intérieur de ce lambda peut faire. Si vous avez un code:

 Task.Run(async delegate { SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx()); await SomethingAsync(); //    SomeCoolSyncCtx }); 

alors le code à l'intérieur SomethingAsyncverra réellement l' SynchronizationContext.Currentinstance SomeCoolSyncCtx. et cela await, et toutes les attentes non configurées dans SomethingAsync seront renvoyées dans ce contexte. Ainsi, pour utiliser cette approche, il est nécessaire de comprendre ce que tout le code que vous mettez dans la file d'attente peut ou ne peut pas faire, et si ses actions peuvent devenir un obstacle.

Cette approche se produit également en raison de la nécessité de créer / mettre en file d'attente un objet de tâche supplémentaire. Cela peut ou non être important pour l'application / la bibliothèque, selon les exigences de performances.

Gardez également à l'esprit que ces solutions de contournement peuvent causer plus de problèmes que d'avantages et avoir des conséquences inattendues différentes. Par exemple, certains outils d'analyse statique ConfigureAwait(false) signalent les attentes qui n'utilisent pas CA2007 . Si vous allumez l'analyseur, puis utilisez une telle astuce pour éviter l'utilisation ConfigureAwait, il y a une forte probabilité que l'analyseur le marque. Cela peut impliquer encore plus de travail, par exemple, vous pouvez désactiver l'analyseur en raison de son importunité, ce qui impliquera de sauter d'autres endroits de la base de code où vous devez vraiment l'utiliser ConfigureAwait(false).

Est-il possible d'utiliser SynchronizationContext.SetSynchronizationContext pour Ă©viter d'utiliser ConfigureAwait (false)?


Non.Bien que ce soit possible. Cela dépend de l'implémentation utilisée.

Certains développeurs le font:

 Task t; SynchronizationContext old = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); try { t = CallCodeThatUsesAwaitAsync(); // await'      } finally { SynchronizationContext.SetSynchronizationContext(old); } await t; //  -     


dans l'espoir que cela forcera le code à l'intérieur à CallCodeThatUsesAwaitAsyncvoir le contexte actuel comme null. Il en sera ainsi. Cependant, cette option n'affectera pas celle qu'elle awaitvoit TaskScheduler.Current. Par conséquent, si le code est exécuté dans un spécial TaskScheduler, c'est à awaitl'intérieur CallCodeThatUsesAwaitAsyncqu'il verra et fera la queue pour ce spécial TaskScheduler.

Comme dans la Task.RunFAQ, les mêmes mises en garde s'appliquent ici: il y a certaines conséquences de cette approche, et le code à l'intérieur du bloc trypeut également interférer avec ces tentatives en définissant un contexte différent (ou en appelant le code à l'aide d'un planificateur de tâches non standard).

Avec ce modèle, vous devez également faire attention aux modifications mineures:

 SynchronizationContext old = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); try { await t; } finally { SynchronizationContext.SetSynchronizationContext(old); } 

Vous voyez quel est le problème? Un peu difficile à remarquer, mais c'est impressionnant. Il n'y a aucune garantie que l'attente finira par provoquer un rappel / continuer dans le thread d'origine. Cela signifie que le retour SynchronizationContextà l'original peut ne pas se produire dans le thread d'origine, ce qui peut conduire au fait que les éléments de travail suivants dans ce thread verront le mauvais contexte. Pour contrer cela, des modèles d'application bien écrits qui définissent un contexte spécial ajoutent généralement du code pour le réinitialiser manuellement avant d'appeler un code personnalisé supplémentaire. Et même si cela se produit dans un thread, cela peut prendre un certain temps pendant lequel le contexte peut ne pas être correctement restauré. Et si cela fonctionne dans un thread différent, cela peut conduire à l'installation du mauvais contexte. Et ainsi de suite. Assez loin d'être idéal.

Dois-je utiliser ConfigureAwait (false) si j'utilise GetAwaiter () .GetResult ()?


Non. ConfigureAwaitaffecte uniquement les rappels. En particulier, le modèle awaiternécessite que vous awaiterfournissiez la propriété IsCompleted, les méthodes GetResultet OnCompleted(éventuellement avec la méthode UnsafeOnCompleted). ConfigureAwaitaffecte uniquement le comportement {Unsafe}OnCompleted, donc si vous appelez directement GetResult(), que vous le fassiez TaskAwaiterou qu'il n'y ait ConfiguredTaskAwaitable.ConfiguredTaskAwaiteraucune différence de comportement. Par conséquent, si vous voyez, task.ConfigureAwait(false).GetAwaiter().GetResult()vous pouvez le remplacer par task.GetAwaiter().GetResult()(en plus, pensez si vous avez vraiment besoin d'une telle implémentation).

Je sais que le code s'exécute dans un environnement dans lequel il n'y aura jamais de SynchronizationContext spécial ou de TaskScheduler spécial. Puis-je ne pas utiliser ConfigureAwait (faux)?


C'est possible. Cela dépend de la confiance que vous avez à l'égard du «jamais». Comme mentionné dans les questions précédentes, ce n'est pas parce que le modèle de l'application dans laquelle vous travaillez ne spécifie pas une application spéciale SynchronizationContextet n'appelle pas votre code dans une application spéciale TaskSchedulerque le code d'un autre utilisateur ou bibliothèque ne les utilise pas. Il faut donc en être sûr, ou du moins reconnaître le risque qu'une telle option soit possible.

J'ai entendu dire que dans .NET Core, il n'est pas nécessaire d'appliquer ConfigureAwait (false). En est-il ainsi?


Pas comme ça.Il est nécessaire lorsque vous travaillez dans .NET Core pour les mêmes raisons que lorsque vous travaillez dans .NET Framework. Rien n'a changé à cet égard.

Cela a changé si certains environnements publient le leur SynchronizationContext. En particulier, alors qu'ASP.NET classique dans le .NET Framework a le sien SynchronizationContext, ASP.NET Core n'en a pas. Cela signifie que le code exécuté dans l'application ASP.NET Core ne verra pas de code spécial par défaut SynchronizationContext, ce qui réduit le besoin ConfigureAwait(false)de cet environnement.

Cependant, cela ne signifie pas qu'il n'y aura jamais de coutume SynchronizationContextouTaskScheduler. Si un code utilisateur (ou un autre code de bibliothèque utilisé par l'application) définit le contexte utilisateur et appelle votre code ou appelle votre code dans la tâche planifiée dans le planificateur de tâches spéciales, awaitASP.NET Core verra un contexte ou un planificateur non standard, ce qui peut nécessiter une utilisation ConfigureAwait(false). Bien sûr, dans les situations où vous évitez les verrous synchrones (ce que vous devez faire dans les applications Web de toute façon) et si vous n'êtes pas contre la faible surcharge de performances dans certains cas, vous pouvez le faire sans utiliser ConfigureAwait(false).

Puis-je utiliser ConfigureAwait lors de «l'attente de la fin de chaque tâche» sur IAsyncEnumerable?


OuiVoir l' article MSDN pour un exemple .

Await foreachcorrespond au modèle et peut donc être utilisé pour répertorier IAsyncEnumerable<T>. Il peut également être utilisé pour répertorier les éléments qui représentent la portée correcte de l'API. bibliothèques d'exécution .NET comprennent un procédé d'expansion ConfigureAwait à IAsyncEnumerable<T>laquelle renvoie un type spécial, qui enveloppe IAsyncEnumerable<T>et Booleancorrespond au modèle correct. Lorsque le compilateur génère des appels à MoveNextAsyncet l' DisposeAsyncénumérateur. Ces appels sont liés au type de structure d'énumérateur configuré retourné, qui à son tour répond aux attentes selon les besoins.

Puis-je utiliser ConfigureAwait avec 'wait using' IAsyncDisposable?


Oui, mais avec une petite complication.

Comme avec IAsyncEnumerable<T>, les bibliothèques d'exécution .NET fournissent une méthode d'extension ConfigureAwaitpour IAsyncDisposableet await usingfonctionneront très bien car elles implémentent le modèle approprié (à savoir, elles fournissent la méthode appropriée DisposeAsync):

 await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false)) { ... } 

Le problème ici est que le type cn'est plus MyAsyncDisposableClass, mais plutôt System.Runtime.CompilerServices.ConfiguredAsyncDisposable, celui renvoyé par la méthode d'extension ConfigureAwaitpour IAsyncDisposable.

Pour contourner cela, ajoutez la ligne:

 var c = new MyAsyncDisposableClass(); await using (c.ConfigureAwait(false)) { ... } 

Maintenant, le type est à cnouveau souhaité MyAsyncDisposableClass. Ce qui a également pour effet d'augmenter les possibilités de c; si nécessaire, vous pouvez tout envelopper dans des accolades.

J'ai utilisé ConfigureAwait (false), mais mon AsyncLocal est toujours entré dans le code après avoir attendu. Est-ce un bug?


Non, c'est assez attendu. Le flux de données AsyncLocal<T>est une partie ExecutionContextdistincte de SynchronizationContext. Si vous ne le faites pas explicitement flux déconnectés à l' ExecutionContextaide ExecutionContext.SuppressFlow(), ExecutionContext(et donc des données AsyncLocal <T>passe) toujours par awaits, quel que soit qu'il soit utilisé ConfigureAwaitpour éviter de capturer la source SynchronizationContext. Plus de détails sont discutés dans cet article .

Les outils linguistiques peuvent-ils m'aider à éviter d'avoir à utiliser explicitement ConfigureAwait (false) dans ma bibliothèque?


Les développeurs de bibliothèques se plaignent parfois de la nécessité d'utiliser ConfigureAwait(false)et demandent des alternatives moins invasives.

Actuellement, ils ne le sont pas, du moins ils ne sont pas intégrés dans le langage / compilateur / runtime. Cependant, il existe de nombreuses suggestions sur la façon dont cela peut être mis en œuvre, par exemple: 1 , 2 , 3 , 4 .

Si le sujet qui vous intéresse, si vous avez des idées nouvelles et intéressantes, l'auteur de l' article original vous invite à une discussion.

Source: https://habr.com/ru/post/fr482354/


All Articles