295 votes

Pourquoi utiliser Task<T> plutôt que ValueTask<T> en C# ?

Depuis C# 7.0, les méthodes asynchrones peuvent renvoyer ValueTask<T>. L'explication dit qu'il faut l'utiliser quand on a un résultat en cache ou qu'on simule l'asynchronisme via du code synchrone. Cependant, je ne comprends toujours pas quel est le problème de l'utilisation systématique de ValueTask ou, en fait, pourquoi async/await n'a pas été construit avec un type de valeur dès le départ. Quand est-ce que ValueTask ne ferait pas l'affaire ?

13 votes

Je soupçonne que ça a à voir avec les avantages de ValueTask<T> (en termes d'allocations) ne se matérialisant pas pour les opérations qui sont en fait asynchrone (car dans ce cas ValueTask<T> aura toujours besoin d'une allocation de tas). Il y a aussi la question de Task<T> ayant beaucoup d'autres soutiens au sein des bibliothèques.

0 votes

Je pensais que les arbitres étaient moins chers. Voir cette question sur le c++ stackoverflow.com/q/42721683/5976576 . Je suis peut-être en train de manquer le bateau.

4 votes

@JonSkeet les bibliothèques existantes sont un problème mais cela pose la question de savoir si Task aurait dû être ValueTask dès le départ ? Les avantages peuvent ne pas exister lorsqu'on l'utilise pour des trucs asynchrones réels, mais est-ce nuisible ?

358voto

Stephen Cleary Points 91731

Desde les documents de l'API (accentuation ajoutée) :

Les méthodes peuvent renvoyer une instance de ce type de valeur lorsqu'il est probable que le résultat de leurs opérations sera disponible de manière synchrone. y lorsque l'on s'attend à ce que la méthode soit invoquée si fréquemment que le coût de l'attribution d'une nouvelle méthode d'évaluation de l'impact sur l'environnement est trop élevé. Task<TResult> pour chaque appel sera prohibitif.

Il y a des inconvénients à utiliser un ValueTask<TResult> au lieu d'un Task<TResult> . Par exemple, alors qu'un ValueTask<TResult> peut aider à éviter une allocation dans le cas où le résultat réussi est disponible de manière synchrone, il contient également deux champs alors qu'un Task<TResult> comme un type de référence est un champ unique. Cela signifie qu'un appel de méthode renvoie deux champs de données au lieu d'un seul, ce qui représente plus de données à copier. Cela signifie également que si une méthode qui renvoie l'un de ces champs est attendue dans un fichier de type async la machine à états de cette méthode async sera plus volumineuse, car il faudra stocker la structure qui comporte deux champs au lieu d'une seule référence.

En outre, pour des utilisations autres que la consommation du résultat d'une opération asynchrone par l'intermédiaire de l'application await , ValueTask<TResult> peut conduire à un modèle de programmation plus alambiqué, qui peut à son tour conduire à davantage d'allocations. Prenons l'exemple d'une méthode qui pourrait renvoyer soit un objet de type Task<TResult> avec une tâche mise en cache comme résultat commun ou une ValueTask<TResult> . Si le consommateur du résultat veut l'utiliser en tant que Task<TResult> comme l'utilisation de méthodes telles que Task.WhenAll y Task.WhenAny le ValueTask<TResult> devra d'abord être converti en un Task<TResult> en utilisant AsTask ce qui conduit à une allocation qui aurait pu être évitée si l'on disposait d'un cache Task<TResult> avait été utilisé en premier lieu.

En tant que tel, le choix par défaut pour toute méthode asynchrone devrait être de renvoyer un fichier de type Task o Task<TResult> . Ce n'est que si l'analyse des performances prouve que cela vaut la peine, qu'une ValueTask<TResult> être utilisé à la place de Task<TResult> .

0 votes

Accepté car il s'agit de la réponse la plus directe à la question et il contient des directives claires sur ce qu'il faut faire.

13 votes

Cela permet d'économiser un seul Task (qui est petite et peu coûteuse de nos jours), mais au prix de la mise en place de la de l'appelant allocation existante plus grande et en doublant la taille de la valeur de retour (ce qui a un impact sur l'allocation des registres). Bien que ce soit un choix clair pour un scénario de lecture en mémoire tampon, l'appliquer par défaut à toutes les interfaces n'est pas quelque chose que je recommande.

4 votes

Oui, soit Task o ValueTask peut être utilisé comme un type de retour synchrone (avec l'option Task.FromResult ). Mais il y a toujours de la valeur (heh) en ValueTask si vous avez quelque chose que vous s'attendre à pour être synchrone. ReadByteAsync étant un exemple classique. I croire ValueTask a été créé principalement pour les nouveaux "canaux" (flux d'octets de bas niveau), éventuellement utilisés aussi dans ASP.NET core où les performances sont vraiment importantes.

156voto

Eric Lippert Points 300275

Cependant, je ne comprends toujours pas quel est le problème d'utiliser ValueTask de manière systématique.

Les types de structures ne sont pas libres. Copier des structures qui sont plus grandes que la taille d'une référence peut être plus lent que de copier une référence. Le stockage des structures qui sont plus grandes qu'une référence prend plus de mémoire que le stockage d'une référence. Les structures dont la taille est supérieure à 64 bits peuvent ne pas être enregistrées alors qu'une référence pourrait l'être. Les avantages d'une pression de collecte plus faible peuvent ne pas dépasser les coûts.

Les problèmes de performance doivent être abordés avec une discipline d'ingénieur. Fixez des objectifs, mesurez vos progrès par rapport aux objectifs, puis décidez comment modifier le programme si les objectifs ne sont pas atteints, en mesurant en cours de route pour vous assurer que vos changements sont réellement des améliorations.

pourquoi async/await n'a pas été construit avec un type de valeur dès le début.

await a été ajouté à C# bien après le Task<T> existait déjà. Il aurait été quelque peu pervers d'inventer un nouveau type alors qu'il en existait déjà un. Et await est passé par de nombreuses itérations de conception avant de s'arrêter sur celle qui a été livrée en 2012. La perfection est l'ennemi du bien ; mieux vaut livrer une solution qui fonctionne bien avec l'infrastructure existante et, si les utilisateurs le demandent, apporter des améliorations plus tard.

Je note également que la nouvelle fonctionnalité permettant aux types fournis par l'utilisateur d'être la sortie d'une méthode générée par le compilateur ajoute un risque et une charge de test considérables. Lorsque les seules choses que vous pouvez retourner sont void ou une tâche, l'équipe de test n'a pas à envisager de scénario dans lequel un type absolument fou est retourné. Tester un compilateur signifie déterminer non seulement quels programmes les gens sont susceptibles d'écrire, mais aussi quels programmes sont susceptibles d'être utilisés. possible à écrire, car nous voulons que le compilateur compile tous les programmes légaux, et pas seulement tous les programmes sensés. C'est cher.

Quelqu'un peut-il expliquer dans quelles circonstances ValueTask ne peut pas faire le travail ?

Le but de la chose est d'améliorer les performances. Ça ne fait pas l'affaire si ça ne fait pas de manière mesurable y de manière significative améliorer les performances. Il n'y a aucune garantie qu'il le fera.

8 votes

Structs that are larger than 64 bits might not be enregistered when a reference could be enregistered ...au cas où quelqu'un d'autre se poserait la question, le mot "enregistré" ici fait probablement référence au fait d'être "stocké dans les registres du CPU" (qui sont les emplacements mémoire les plus rapides possibles).

3 votes

J'ai retiré le lien de cette réponse pour pouvoir l'aimer à nouveau.

35voto

unsafePtr Points 554

Il y a quelques changements dans .Net Core 2.1 . A partir de .net core 2.1 ValueTask peut représenter non seulement les actions synchrones terminées mais aussi les actions asynchrones terminées. De plus, nous recevons des objets non génériques ValueTask type.

Je vais laisser Stephen Toub commentaire ce qui est lié à votre question :

Nous devons encore formaliser les directives, mais je pense que ce sera quelque chose comme ceci pour la surface de l'API publique :

  • C'est la tâche qui offre la plus grande facilité d'utilisation.

  • ValueTask offre le plus grand nombre d'options pour l'optimisation des performances.

  • Si vous écrivez une interface ou une méthode virtuelle que d'autres remplaceront, ValueTask est le bon choix par défaut.

  • Si vous prévoyez que l'API sera utilisée sur des chemins chauds où les allocations auront de l'importance, ValueTask est un bon choix.

  • Sinon, lorsque les performances ne sont pas critiques, choisissez par défaut Task, car il fournit de meilleures performances. garanties et une meilleure convivialité.

Du point de vue de la mise en œuvre, de nombreuses instances ValueTask retournées seront toujours soutenues par Task.

La fonctionnalité peut être utilisée non seulement dans le noyau .net 2.1. Vous pourrez l'utiliser avec System.Threading.Tasks.Extensions paquet.

3 votes

Plus de Stephen aujourd'hui : blogs.msdn.microsoft.com/dotnet/2018/11/07/

23voto

Ed Plunkett Points 1579

ValueTask<T> n'est pas un sous-ensemble de Task<T> c'est un superset .

ValueTask<T> est une union discriminée d'un T et d'un Task<T> ce qui fait qu'il n'y a pas d'allocation pour ReadAsync<T> pour retourner de manière synchrone une valeur T dont il dispose (contrairement à l'utilisation de Task.FromResult<T> qui doit allouer un Task<T> instance). ValueTask<T> est attendable, de sorte que la plupart des consommations d'instances seront indiscernables de celles effectuées avec un fichier Task<T> .

ValueTask, étant une structure, permet d'écrire des méthodes asynchrones qui n'allouent pas de mémoire lorsqu'elles s'exécutent de manière synchrone, sans compromettre la cohérence de l'API. Imaginez que vous ayez une interface avec une méthode de retour Task. Chaque classe implémentant cette interface doit retourner une tâche, même si elle s'exécute de manière synchrone (en espérant utiliser Task.FromResult). Vous pouvez bien sûr avoir deux méthodes différentes sur l'interface, une synchrone et une asynchrone, mais cela nécessite deux implémentations différentes pour éviter "sync over async" et "async over sync".

Il vous permet donc d'écrire une méthode qui est soit asynchrone, soit synchrone, plutôt que d'écrire une méthode identique pour chacune. Vous pouvez l'utiliser partout où vous utilisez Task<T> mais cela n'apporterait souvent rien de plus.

Eh bien, cela ajoute une chose : cela ajoute une promesse implicite à l'appelant que la méthode utilise réellement la fonctionnalité supplémentaire que l'on a ajoutée. ValueTask<T> fournit. Personnellement, je préfère choisir des types de paramètres et de retours qui en disent le plus possible à l'appelant. Ne renvoyez pas IList<T> si l'énumération ne peut pas fournir un compte ; ne pas retourner IEnumerable<T> si elle le peut. Vos consommateurs ne devraient pas avoir à consulter de documentation pour savoir quelles méthodes peuvent raisonnablement être appelées de manière synchrone et lesquelles ne le peuvent pas.

Je ne vois pas dans les futurs changements de conception un argument convaincant. C'est plutôt le contraire : Si une méthode change sa sémantique, cela signifie que devrait interrompre la construction jusqu'à ce que tous les appels soient mis à jour en conséquence. Si cela est considéré comme indésirable (et croyez-moi, je suis sensible au désir de ne pas casser le build), envisagez le versioning de l'interface.

C'est essentiellement à cela que sert le typage fort.

Si certains des programmeurs qui conçoivent des méthodes asynchrones dans votre atelier ne sont pas en mesure de prendre des décisions éclairées, il peut être utile d'assigner un mentor senior à chacun de ces programmeurs moins expérimentés et d'organiser une revue de code hebdomadaire. S'ils se trompent, expliquez-leur pourquoi il faut procéder différemment. C'est une surcharge pour les seniors, mais cela permettra aux juniors de se mettre à niveau beaucoup plus rapidement que si vous les jetiez dans le grand bain en leur donnant une règle arbitraire à suivre.

Si le gars qui a écrit la méthode ne sait pas si elle peut être appelée de manière synchrone, qui sur terre le fait ? !

Si vous avez autant de programmeurs inexpérimentés qui écrivent des méthodes asynchrones, ces mêmes personnes les appellent-elles aussi ? Sont-elles qualifiées pour déterminer par elles-mêmes celles qui sont sûres d'être appelées asynchrones, ou vont-elles commencer à appliquer une règle tout aussi arbitraire à la façon dont elles appellent ces choses ?

Le problème ici n'est pas vos types de retour, c'est que les programmeurs sont mis dans des rôles pour lesquels ils ne sont pas prêts. Cela a dû se produire pour une raison, donc je suis sûr que cela ne peut pas être trivial à réparer. Le décrire n'est certainement pas une solution. Mais chercher un moyen de faire passer le problème en douce au compilateur n'est pas non plus une solution.

9voto

Shubhan Points 344

Une info plus récente de Marc (août 2019)

Utilisez Task lorsque quelque chose est généralement ou toujours asynchrone, c'est-à-dire qu'il n'est pas immédiatement terminé ; utilisez ValueTask lorsque quelque chose est généralement ou toujours synchrone, c'est-à-dire que la valeur sera connue en ligne ; utilisez également ValueTask dans un scénario polymorphe (virtuel, interface) où vous ne pouvez pas connaître la réponse.

Source : https://blog.marcgravell.com/2019/08/prefer-valuetask-to-task-always-and.html

J'ai suivi l'article du blog ci-dessus pour un projet récent où j'avais des questions similaires.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X