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 SynchronizationContext
code 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 await
ne 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 await
se terminera toujours de manière asynchrone et que l'opération attendue la rappellera dans un environnement exempt de spécial SynchronizationContext
ou TaskScheduler
. Par exemple, CryptoStream
dans 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 unawaiter
pour 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.Run
exécuter dans un pool de threads de flux, de sorte qu'aucune modification du code ci - dessus, SynchronizationContext.Current
renvoie la valeur null
. De plus, il Task.Run
utilise implicitement TaskScheduler.Default
, par conséquent, TaskScheduler.Current
à l'intérieur du délégué retournera également une valeur Default
. Cela signifie qu'il await
aura 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 SomethingAsync
verra réellement l' SynchronizationContext.Current
instance 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 à CallCodeThatUsesAwaitAsync
voir le contexte actuel comme null
. Il en sera ainsi. Cependant, cette option n'affectera pas celle qu'elle await
voit TaskScheduler.Current
. Par conséquent, si le code est exécuté dans un spécial TaskScheduler
, c'est Ă await
l'intérieur CallCodeThatUsesAwaitAsync
qu'il verra et fera la queue pour ce spécial TaskScheduler
.Comme dans la Task.Run
FAQ, 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 try
peut é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.
ConfigureAwait
affecte uniquement les rappels. En particulier, le modèle awaiter
nécessite que vous awaiter
fournissiez la propriété IsCompleted
, les méthodes GetResult
et OnCompleted
(éventuellement avec la méthode UnsafeOnCompleted). ConfigureAwait
affecte uniquement le comportement {Unsafe}OnCompleted
, donc si vous appelez directement GetResult()
, que vous le fassiez TaskAwaiter
ou qu'il n'y ait ConfiguredTaskAwaitable.ConfiguredTaskAwaiter
aucune 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 SynchronizationContext
et n'appelle pas votre code dans une application spéciale TaskScheduler
que 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 SynchronizationContext
ouTaskScheduler
. 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, await
ASP.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 foreach
correspond 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 Boolean
correspond au modèle correct. Lorsque le compilateur génère des appels à MoveNextAsync
et 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 ConfigureAwait
pour IAsyncDisposable
et await using
fonctionneront 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 c
n'est plus MyAsyncDisposableClass
, mais plutĂ´t System.Runtime.CompilerServices.ConfiguredAsyncDisposable
, celui renvoyé par la méthode d'extension ConfigureAwait
pour IAsyncDisposable
.Pour contourner cela, ajoutez la ligne: var c = new MyAsyncDisposableClass(); await using (c.ConfigureAwait(false)) { ... }
Maintenant, le type est Ă c
nouveau 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 ExecutionContext
distincte de SynchronizationContext
. Si vous ne le faites pas explicitement flux déconnectés à l' ExecutionContext
aide ExecutionContext.SuppressFlow()
, ExecutionContext
(et donc des données AsyncLocal <T>
passe) toujours par awaits
, quel que soit qu'il soit utilisé ConfigureAwait
pour é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.