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

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);
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();
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();
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?
Oui
Voir 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.