Programmation asynchrone - performances asynchrones: comprendre les coûts de l'async et attendre

Cet article est assez ancien, mais n'a pas perdu sa pertinence. En ce qui concerne async / wait, un lien vers celui-ci apparaît généralement. Je n'ai pas trouvé de traduction en russe, j'ai décidé d'aider quelqu'un qui ne parle pas couramment.




La programmation asynchrone a longtemps été le royaume des développeurs les plus expérimentés qui ont soif de masochisme - ceux qui avaient assez de temps libre, d'inclination et de capacité psychique pour penser aux rappels des rappels dans un flux d'exécution non linéaire. Avec l'avènement de Microsoft .NET Framework 4.5, C # et Visual Basic nous ont tous apporté l'asynchronie, de sorte que les simples mortels peuvent désormais écrire des méthodes asynchrones presque aussi facilement que des méthodes synchrones. Les rappels ne sont plus nécessaires. Plus de code de marshaling explicite d'un contexte de synchronisation à un autre. Plus de soucis sur la façon dont les résultats d'exécution ou les exceptions évoluent. Il n'y a pas besoin d'astuces qui déforment les moyens de langages de programmation pour la commodité de développer du code asynchrone. Bref, il n'y a plus de problèmes et de maux de tête.


Bien sûr, bien qu'il soit maintenant facile de commencer à écrire des méthodes asynchrones (voir les articles d'Eric Lippert et Mads Torgersen dans ce MSDN Magazine [OCTOBRE 2011] ), une compréhension est nécessaire pour le faire correctement. ce qui se passe sous le capot. Chaque fois qu'un langage ou une bibliothèque augmente le niveau d'abstraction qu'un développeur peut utiliser, cela s'accompagne inévitablement de coûts cachés qui réduisent la productivité. Dans de nombreux cas, ces coûts sont négligeables, de sorte qu'ils peuvent être négligés dans la plupart des cas par la plupart des programmeurs. Cependant, les développeurs avancés doivent bien comprendre quels sont les coûts présents afin de prendre les mesures nécessaires et de résoudre les problèmes éventuels s'ils se manifestent. Cela est requis lors de l'utilisation d'outils de programmation asynchrone en C # et Visual Basic.


Dans cet article, je vais décrire les entrées et les sorties des méthodes asynchrones, expliquer comment les méthodes asynchrones sont implémentées et discuter de certains des coûts les plus faibles. Notez que ce n'est pas une recommandation de déformer le code lisible en quelque chose qui est difficile à maintenir, au nom de la microoptimisation et des performances. Ce ne sont que les connaissances qui vous aideront à diagnostiquer les problèmes que vous pouvez rencontrer et un ensemble d'outils pour surmonter ces problèmes. En outre, cet article est basé sur l'aperçu de la version 4.5 de .NET Framework, et probablement les détails d'implémentation spécifiques peuvent changer dans la version finale.


Obtenez un modèle de réflexion confortable


Depuis des décennies, les programmeurs utilisent des langages de programmation de haut niveau C #, Visual Basic, F # et C ++ pour développer des applications productives. Cette expérience a permis aux programmeurs d'évaluer les coûts de diverses opérations et d'acquérir des connaissances sur les meilleures techniques de développement. Par exemple, dans la plupart des cas, l'invocation d'une méthode synchrone est relativement économique, surtout si le compilateur peut incorporer le contenu de la méthode invoquée directement dans le point d'appel. Par conséquent, les développeurs sont habitués à diviser le code en petites méthodes faciles à entretenir, sans avoir à se soucier des conséquences négatives de l'augmentation du nombre d'appels. Le modèle de réflexion de ces programmeurs est conçu pour gérer les appels de méthode.


Avec l'avènement des méthodes asynchrones, un nouveau modèle de pensée s'impose. C # et Visual Basic avec leurs compilateurs sont capables de créer l'illusion que la méthode asynchrone fonctionne comme son homologue synchrone, bien que tout soit complètement faux à l'intérieur. Le compilateur génère une énorme quantité de code pour le programmeur, très similaire au modèle standard que les développeurs ont écrit pour prendre en charge l'asynchronie pendant le temps où il était nécessaire de le faire à la main. De plus, le code généré par le compilateur contient des appels aux fonctions de la bibliothèque .NET Framework, ce qui réduit encore la quantité de travail qu'un programmeur doit effectuer. Afin d'avoir le bon modèle de pensée et de l'utiliser pour prendre des décisions éclairées, il est important de comprendre ce que le compilateur génère pour vous.


Plus de méthodes, moins d'appels


Lorsque vous travaillez avec du code synchrone, l'exécution de méthodes avec un contenu vide est pratiquement sans valeur. Pour les méthodes asynchrones, ce n'est pas le cas. Considérez cette méthode asynchrone, composée d'une instruction (et qui, en raison du manque d'instructions d'attente, sera exécutée de manière synchrone):


public static async Task SimpleBodyAsync() { Console.WriteLine("Hello, Async World!"); } 

Un décompilateur de langage intermédiaire (IL) révélera le vrai contenu de cette fonction après la compilation, produisant quelque chose de similaire à la figure 1. Ce qui était un simple liner transformé en deux méthodes, dont l'une appartient à la classe auxiliaire de la machine à états. La première est une méthode stub qui a une signature similaire à celle écrite par le programmeur (cette méthode a le même nom, la même portée, elle prend les mêmes paramètres et renvoie le même type), mais ne contient pas de code écrit par le programmeur. Il ne contient qu'un passe-partout standard pour la configuration initiale. Le code de configuration initiale initialise la machine d'état nécessaire pour représenter la méthode asynchrone et la démarre à l'aide d'un appel à la méthode de l'utilitaire MoveNext. Le type d'objet de la machine d'état contient une variable avec l'état d'exécution de la méthode asynchrone, vous permettant de l'enregistrer lors du basculement entre les points d'attente asynchrones. Il contient également du code écrit par un programmeur, modifié pour assurer le transfert des résultats d'exécution et des exceptions à l'objet Task retourné; maintien de la position actuelle dans la méthode afin que l'exécution puisse continuer à partir de cette position après la reprise, etc.


Figure 1 modèle de méthode asynchrone


 [DebuggerStepThrough] public static Task SimpleBodyAsync() { <SimpleBodyAsync>d__0 d__ = new <SimpleBodyAsync>d__0(); d__.<>t__builder = AsyncTaskMethodBuilder.Create(); d__.MoveNext(); return d__.<>t__builder.Task; } [CompilerGenerated] [StructLayout(LayoutKind.Sequential)] private struct <SimpleBodyAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public void MoveNext() { try { if (this.<>1__state == -1) return; Console.WriteLine("Hello, Async World!"); } catch (Exception e) { this.<>1__state = -1; this.<>t__builder.SetException(e); return; } this.<>1__state = -1; this.<>t__builder.SetResult(); } ... } 

Lorsque vous vous demandez combien coûtent les appels aux méthodes asynchrones, souvenez-vous de ce modèle. Le bloc try / catch dans la méthode MoveNext est nécessaire pour empêcher une éventuelle tentative d'incorporation de cette méthode JIT par le compilateur, donc au moins nous obtenons le coût de l'appel de la méthode, tandis que lors de l'utilisation de la méthode synchrone, cet appel ne le sera probablement pas (à condition que contenu minimaliste). Nous recevrons plusieurs appels aux procédures Framework (par exemple, SetResult). Ainsi que plusieurs opérations d'écriture dans les champs de l'objet machine d'état. Bien sûr, nous devons comparer tous ces coûts avec les coûts de Console.WriteLine, qui prévaudront probablement (ils incluent les coûts de verrouillage, d'E / S, etc.) Faites attention aux optimisations que l'environnement fait pour vous. Par exemple, un objet de machine d'état est implémenté en tant que struct. Cette structure sera encadrée dans un tas géré uniquement si la méthode doit interrompre l'exécution, en attendant la fin de l'opération, et cela ne se produira jamais dans cette méthode simple. Ainsi, le modèle de cette méthode asynchrone ne nécessitera pas d'allocation de mémoire à partir du tas. Le compilateur et le runtime tenteront de minimiser le nombre d'opérations d'allocation de mémoire.


Quand ne pas utiliser Async


Le .NET Framework essaie de générer des implémentations efficaces pour les méthodes asynchrones à l'aide de diverses méthodes d'optimisation. Néanmoins, les développeurs, basés sur leur expérience, appliquent souvent leurs méthodes d'optimisation, qui peuvent être risquées et peu pratiques pour l'automatisation par le compilateur et le runtime, car ils essaient d'utiliser des approches universelles. Si vous ne l'oubliez pas, le refus d'utiliser des méthodes asynchrones est bénéfique dans un certain nombre de cas spécifiques, en particulier, cela s'applique aux méthodes dans les bibliothèques qui peuvent être utilisées avec des paramètres plus fins. Habituellement, cela se produit lorsqu'il est certain que la méthode peut être exécutée de manière synchrone, car les données dont elle dépend sont déjà prêtes.


Lors de la création de méthodes asynchrones, les développeurs de .NET Framework ont ​​passé beaucoup de temps à optimiser le nombre d'opérations de gestion de la mémoire. Cela est nécessaire car la gestion de la mémoire entraîne le coût le plus élevé dans les performances d'une infrastructure asynchrone. L'opération d'allocation de mémoire pour un objet est généralement relativement peu coûteuse. Allouer de la mémoire aux objets revient à remplir le panier de produits au supermarché - vous ne dépensez rien lorsque vous les mettez dans le panier. Les dépenses se produisent lorsque vous payez à la caisse, sortez votre portefeuille et donnez de l'argent décent. Et si l'allocation de mémoire est facile, le garbage collection ultérieur peut gravement affecter les performances des applications. Lorsque vous démarrez le garbage collection, l'analyse et le marquage des objets qui se trouvent actuellement en mémoire mais qui n'ont pas de liens sont effectués. Plus il y a d'objets placés, plus il faut de temps pour les marquer. De plus, plus le nombre d'objets de grande taille placés est grand, plus la collecte des ordures est souvent nécessaire. Cet aspect du travail avec la mémoire a un impact global sur le système: plus les ordures sont produites par des méthodes asynchrones, plus l'application s'exécute lentement, même si les microtests ne présentent pas de coûts importants.


Pour les méthodes asynchrones qui suspendent leur exécution (en attente de données qui ne sont pas encore prêtes), l'environnement doit créer un objet de type Task, qui sera renvoyé par la méthode, car cet objet sert de référence unique à l'appel. Cependant, des appels de méthode asynchrones peuvent souvent être effectués sans suspension. Ensuite, le runtime peut renvoyer du cache l'objet Task précédemment terminé, qui est utilisé à plusieurs reprises sans avoir à créer de nouveaux objets Task. Vrai, cela n'est autorisé que dans certaines conditions, par exemple, lorsque la méthode asynchrone renvoie une tâche, une tâche ou un objet non universel (non générique) ou lorsque la tâche universelle est spécifiée par un type de référence TResult et que null est renvoyé par la méthode. Bien que la liste de ces conditions s'allonge au fil du temps, il est préférable de savoir comment l'opération est mise en œuvre.

Considérez une implémentation de ce type comme un MemoryStream. MemoryStream est hérité de Stream et redéfinit les nouvelles méthodes implémentées dans .NET 4.5: ReadAsync, WriteAsync et FlushAsync, afin de fournir une optimisation de code spécifique à la mémoire. Étant donné que l'opération de lecture est effectuée à partir d'un tampon situé en mémoire, c'est-à-dire qu'il s'agit en fait d'une copie de la zone de mémoire, les meilleures performances seront si ReadAsync est exécuté en mode synchrone. Une implémentation de ceci dans une méthode asynchrone pourrait ressembler à ceci:


 public override async Task<int> ReadAsync(byte [] buffer, int offset, int count, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return this.Read(buffer, offset, count); } 

Assez simple. Et puisque Read est un appel synchrone et que la méthode n'a pas d'instructions d'attente pour contrôler les attentes, tous les appels à ce ReadAsync seront en fait exécutés de manière synchrone. Examinons maintenant un cas standard d'utilisation de threads, par exemple, une opération de copie:


 byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) { await source.WriteAsync(buffer, 0, numRead); } 

Veuillez noter que dans l'exemple ReadAsync donné, le flux source est toujours appelé avec le même paramètre de longueur de tampon, ce qui signifie qu'il est très probable que la valeur de retour (le nombre d'octets lus) sera également répétée. Sauf dans de rares circonstances, l'implémentation de ReadAsync est peu susceptible d'utiliser l'objet Task mis en cache comme valeur de retour, mais vous pouvez le faire.


Considérez une autre option d'implémentation pour cette méthode, illustrée à la figure 2. En utilisant les avantages de ses aspects inhérents aux scripts standard pour cette méthode, nous pouvons optimiser l'implémentation en excluant les opérations d'allocation de mémoire, ce qui est peu probable à partir de l'exécution. Nous pouvons éliminer complètement la perte de mémoire en renvoyant le même objet Task qui a été utilisé lors de l'appel ReadAsync précédent si le même nombre d'octets a été lu. Et pour une telle opération de bas niveau, qui est susceptible d'être très rapide et sera appelée à plusieurs reprises, cette optimisation aura un effet significatif, en particulier sur le nombre de garbage collections.


Figure 2 Optimisation de la création de tâches


 private Task<int> m_lastTask; public override Task<int> ReadAsync(byte [] buffer, int offset, int count, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { var tcs = new TaskCompletionSource<int>(); tcs.SetCanceled(); return tcs.Task; } try { int numRead = this.Read(buffer, offset, count); return m_lastTask != null && numRead == m_lastTask.Result ? m_lastTask : (m_lastTask = Task.FromResult(numRead)); } catch(Exception e) { var tcs = new TaskCompletionSource<int>(); tcs.SetException(e); return tcs.Task; } } 

Une méthode d'optimisation similaire en éliminant la création inutile d'objets Task peut être utilisée si la mise en cache est nécessaire. Envisagez une méthode conçue pour récupérer le contenu d'une page Web et le mettre en cache pour référence future. En tant que méthode asynchrone, cela peut être écrit comme suit (en utilisant la nouvelle bibliothèque System.Net.Http.dll pour .NET 4.5):


 private static ConcurrentDictionary<string,string> s_urlToContents; public static async Task<string> GetContentsAsync(string url) { string contents; if (!s_urlToContents.TryGetValue(url, out contents)) { var response = await new HttpClient().GetAsync(url); contents = response.EnsureSuccessStatusCode().Content.ReadAsString(); s_urlToContents.TryAdd(url, contents); } return contents; } 

Il s'agit d'une implémentation frontale. Et pour les appels GetContentsAsync qui ne trouvent pas de données dans le cache, la surcharge de création d'un nouvel objet Task peut être négligée par rapport au coût de réception de données sur le réseau. Cependant, dans le cas de l'obtention de données à partir du cache, ces coûts deviennent importants si vous enveloppez simplement et donnez les données locales disponibles.

Pour éliminer ces coûts (si nécessaire pour obtenir des performances élevées), vous pouvez réécrire la méthode comme indiqué dans la figure 3. Nous avons maintenant deux méthodes: une méthode publique synchrone et une méthode privée asynchrone, à laquelle le public délègue. La collection Dictionary met désormais en cache les objets Task créés, et non leur contenu, de sorte que les futures tentatives de récupération du contenu d'une page précédemment obtenue avec succès peuvent être effectuées en accédant simplement à la collection pour renvoyer l'objet Task existant. À l'intérieur, vous pouvez utiliser les méthodes ContinueWith de l'objet Task, ce qui nous permet d'enregistrer l'objet exécuté dans la collection - au cas où le chargement de la page aurait réussi. Bien sûr, ce code est plus complexe et nécessite beaucoup de développement et de support, comme d'habitude lors de l'optimisation des performances: vous ne voulez pas passer du temps à l'écrire jusqu'à ce que les tests de performances montrent que ces complications conduisent à son amélioration, ce qui est impressionnant et évident. Les améliorations dépendront en fait de la méthode d'application. Vous pouvez prendre une suite de tests qui simule des cas d'utilisation courants et évaluer les résultats pour déterminer si le jeu en vaut la chandelle.


Figure 3 Mise en cache manuelle des tâches


 private static ConcurrentDictionary<string,Task<string>> s_urlToContents; public static Task<string> GetContentsAsync(string url) { Task<string> contents; if (!s_urlToContents.TryGetValue(url, out contents)) { contents = GetContentsInternalAsync(url); contents.ContinueWith(delegate { s_urlToContents.TryAdd(url, contents); }, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuatOptions.ExecuteSynchronously, TaskScheduler.Default); } return contents; } private static async Task<string> GetContentsInternalAsync(string url) { var response = await new HttpClient().GetAsync(url); return response.EnsureSuccessStatusCode().Content.ReadAsString(); } 

Une autre méthode d'optimisation associée aux objets Task consiste à déterminer s'il faut renvoyer un tel objet à partir de la méthode asynchrone. C # et Visual Basic prennent en charge les méthodes asynchrones qui renvoient une valeur nulle (void) et ne créent pas du tout d'objets Task. Les méthodes asynchrones dans les bibliothèques doivent toujours renvoyer Task et Task, car lors de la conception d'une bibliothèque, vous ne pouvez pas savoir qu'elles ne seront pas utilisées en attendant la fin. Cependant, lors du développement d'applications, les méthodes qui renvoient void peuvent trouver leur place. La principale raison de l'existence de ces méthodes est de fournir des environnements pilotés par événements existants, tels que ASP.NET et Windows Presentation Foundation (WPF). En utilisant async et wait, ces méthodes facilitent l'implémentation des gestionnaires de boutons, des événements de chargement de page, etc. Si vous avez l'intention d'utiliser une méthode asynchrone avec void, soyez prudent avec la gestion des exceptions: les exceptions de celle-ci apparaîtront dans tout SynchronizationContext qui était actif au moment où la méthode a été appelée.

N'oubliez pas le contexte


Il existe de nombreux contextes différents dans le .NET Framework: LogicalCallContext, SynchronizationContext, HostExecutionContext, SecurityContext, ExecutionContext et autres (leur montant gigantesque peut suggérer que les créateurs du Framework étaient financièrement motivés pour créer de nouveaux contextes, mais je suis sûr que ce n'est pas le cas). Certains de ces contextes affectent fortement les méthodes asynchrones, non seulement en termes de fonctionnalités, mais également en termes de performances.


SynchronizationContext SynchronizationContext joue un rôle important pour les méthodes asynchrones. Un «contexte de synchronisation» n'est qu'une abstraction pour garantir qu'un appel de délégué avec les spécificités d'une bibliothèque ou d'un environnement particulier est marshalé. Par exemple, WPF a un DispatcherSynchronizationContext pour représenter un thread d'interface utilisateur (UI) pour Dispatcher: l'envoi d'un délégué à ce contexte de synchronisation entraîne la mise en file d'attente de ce délégué pour l'exécution par le Dispatcher dans son thread. ASP.NET fournit un AspNetSynchronizationContext qui est utilisé pour garantir que les opérations asynchrones impliquées dans le traitement d'une demande ASP.NET sont garanties d'être exécutées séquentiellement et sont liées à l'état HttpContext correct. Eh bien, etc. En général, il existe environ 10 spécialisations de SynchronizationContext dans le .NET Framework, certaines ouvertes, d'autres internes.


Lors de l'attente de tâches ou d'objets d'autres types pour lesquels le .NET Framework peut implémenter cela, les objets qui les attendent (par exemple, TaskAwaiter) capturent le SynchronizationContext en cours au moment où l'attente (attend) commence. À la fin de l'attente, si le SynchronizationContext a été capturé, la poursuite de la méthode asynchrone est envoyée à ce contexte de synchronisation. Pour cette raison, les programmeurs écrivant des méthodes asynchrones qui sont appelées à partir du flux d'interface utilisateur n'ont pas besoin de marshaler manuellement les appels vers le flux d'interface utilisateur pour mettre à jour les contrôles d'interface utilisateur: le Framework effectue ce marshaling automatiquement.


Malheureusement, ce marshaling a un prix. Pour les développeurs d'applications qui utilisent attendent pour implémenter leur flux de contrôle, le marshaling automatique est la bonne solution. Les bibliothèques ont souvent une histoire complètement différente. Pour les développeurs d'applications, ce marshaling est principalement nécessaire pour que le code contrôle le contexte dans lequel il est exécuté, par exemple, pour accéder aux contrôles d'interface utilisateur ou pour accéder au HttpContext correspondant à la demande ASP.NET requise. Cependant, les bibliothèques ne sont généralement pas tenues de satisfaire à une telle exigence. Par conséquent, le marshaling automatique entraîne souvent des coûts supplémentaires complètement inutiles. Jetons un autre regard sur le code qui copie les données d'un flux à un autre:


 byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) { await source.WriteAsync(buffer, 0, numRead); } 

Si cette copie est appelée à partir du flux d'interface utilisateur, chaque opération de lecture et d'écriture forcera l'exécution à revenir au flux d'interface utilisateur. Dans le cas d'un mégaoctet de données dans la source et les flux qui lisent et écrivent de manière asynchrone (c'est-à-dire la plupart de leurs implémentations), cela signifie environ 500 commutateurs du flux d'arrière-plan vers le flux d'interface utilisateur. Pour gérer ce comportement dans les types de tâche et de tâche, la méthode ConfigureAwait est créée. Cette méthode accepte le paramètre continueOnCapturedContext d'un type booléen qui contrôle le marshaling. Si true (valeur par défaut), attendre renvoie automatiquement le contrôle au SynchronizationContext capturé. Si false est utilisé, le contexte de synchronisation sera ignoré et l'environnement continuera à exécuter l'opération asynchrone dans le thread où il a été interrompu. L'implémentation de cette logique donnera une version plus efficace du code de copie entre les threads:

 byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) { await source.WriteAsync(buffer, 0, numRead).ConfigureAwait(false); } 

Pour les développeurs de bibliothèques, une telle accélération suffit en soi pour toujours penser à utiliser ConfigureAwait, à l'exception de rares conditions dans lesquelles la bibliothèque en sait suffisamment sur le runtime et devra exécuter la méthode avec accès au contexte correct.


Outre les performances, il existe une autre raison pour laquelle vous devez utiliser ConfigureAwait lors du développement de bibliothèques. Imaginez que la méthode CopyStreamToStreamAsync, implémentée avec une version de code sans ConfigureAwait, est appelée à partir d'un flux d'interface utilisateur dans WPF, par exemple, comme ceci:


 private void button1_Click(object sender, EventArgs args) { Stream src = …, dst = …; Task t = CopyStreamToStreamAsync(src, dst); t.Wait(); // deadlock! } 

Dans ce cas, le programmeur devait écrire button1_Click en tant que méthode asynchrone dans laquelle l'opérateur attend est censé exécuter la tâche, et ne pas utiliser la méthode d'attente synchrone de cet objet. La méthode Wait doit être utilisée dans de nombreux autres cas, mais ce sera presque toujours une erreur de l'utiliser pour attendre dans un flux d'interface utilisateur, comme indiqué ici. La méthode Wait ne reviendra pas tant que la tâche n'est pas terminée. Dans le cas de CopyStreamToStreamAsync, son flux asynchrone tente de renvoyer l'exécution avec l'envoi de données au SynchronizationContext capturé, et ne peut pas terminer tant que ces transferts ne sont pas terminés (car ils sont nécessaires pour continuer son fonctionnement). Mais ces expéditions, à leur tour, ne peuvent pas être exécutées, car le thread d'interface utilisateur qui doit les gérer est bloqué par l'appel d'attente. Il s'agit d'une dépendance cyclique conduisant à un blocage. Si CopyStreamToStreamAsync est implémenté avec ConfigureAwait (false), il n'y aura ni dépendance ni blocage.


ExecutionContext ExecutionContext est une partie importante du .NET Framework, mais la plupart des programmeurs ignorent encore parfaitement son existence. ExecutionContext – , SecurityContext LogicalCallContext, , . , ThreadPool.QueueUserWorkItem, Task.Run, Delegate.BeginInvoke, Stream.BeginRead, WebClient.DownloadStringAsync Framework, ExecutionContext ExecutionContext.Run ( ). , , ThreadPool.QueueUserWorkItem, Windows (identity), WaitCallback. , Task.Run LogicalCallContext, LogicalCallContext Action. ExecutionContext .


Framework , ExecutionContext, , . Windows LogicalCallContext . (WindowsIdentity.Impersonate CallContext.LogicalSetData) .



. C# Visual Basic , . await. , , - . C# Visual Basic («») , await (boxed) , .


. , . , , , .


C# Visual Basic , . ,


 public static async Task FooAsync() { var dto = DateTimeOffset.Now; var dt = dto.DateTime; await Task.Yield(); Console.WriteLine(dt); } 

dto await, . , , - dto:


Figure 4


 [StructLayout(LayoutKind.Sequential), CompilerGenerated] private struct <FooAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public DateTimeOffset <dto>5__1; public DateTime <dt>5__2; private object <>t__stack; private object <>t__awaiter; public void MoveNext(); [DebuggerHidden] public void <>t__SetMoveNextDelegate(Action param0); } 

, . , , , , . , :


 public static async Task FooAsync() { var dt = DateTimeOffset.Now.DateTime; await Task.Yield(); Console.WriteLine(dt); } 

, .NET (GC) , , , : 0, , , (.NET GC 0, 1 2). , GC . , , , , , , . 0, , , . , , , .


( , ). JIT , , , , . , , . , , , , . , , . , C# Visual Basic , , .



C# Visual Basic , awaits: . await , Task , , . , , :

 public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { return Sum(await a, await b, await c); } private static int Sum(int a, int b, int c) { return a + b + c; } 

C# “await b” Sum. await, Sum, - async , «» await. , await . , , CLR, , , . , <>t__stack. , , Tuple<int, int> <>__stack. , , , . , SumAsync :


 public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { int ra = await a; int rb = await b; int rc = await c; return Sum(ra, rb, rc); } 

, ra, rb rc, . , : . , , , . , , , , .


, , . Sum , await , . , await , . await , Task.WhenAll:


 public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { int [] results = await Task.WhenAll(a, b, c); return Sum(results[0], results[1], results[2]); } 

Task.WhenAll Task<TResult[]>, , , , . . , WhenAll, Task Task. , , , , , WhenAll , . WhenAll, , , params, . , , . Figure 5

Figure 5


 public static Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { return (a.Status == TaskStatus.RanToCompletion && b.Status == TaskStatus.RanToCompletion && c.Status == TaskStatus.RanToCompletion) ? Task.FromResult(Sum(a.Result, b.Result, c.Result)) : SumAsyncInternal(a, b, c); } private static async Task<int> SumAsyncInternal(Task<int> a, Task<int> b, Task<int> c) { await Task.WhenAll((Task)a, b, c).ConfigureAwait(false); return Sum(a.Result, b.Result, c.Result); } 


, . , . , . , , : , , / , . .NET Framework , . , .NET Framework, . , , Framework, , , .

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


All Articles