web-dev-qa-db-fra.com

Pourquoi utiliser la tâche <T> sur ValueTask <T> en C #?

A partir de C # 7.0, les méthodes asynchrones peuvent renvoyer ValueTask <T>. L'explication dit qu'il devrait être utilisé lorsque nous avons un résultat mis en cache ou simulé de manière asynchrone via un code synchrone. Cependant, je ne comprends toujours pas quel est le problème avec l'utilisation de ValueTask toujours ou en fait pourquoi async/wait n'a pas été construit avec un type de valeur depuis le début. Quand ValueTask échouerait-il?

130
Stilgar

De les docs de l'API (soulignement ajouté):

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 et lorsque la méthode doit être invoquée. il est fréquent que le coût d’attribution d’un nouveau Task<TResult> à chaque appel soit prohibitif.

L'utilisation d'un ValueTask<TResult> au lieu d'un Task<TResult> présente des compromis. Par exemple, alors qu'un ValueTask<TResult> peut aider à éviter une allocation dans le cas où le résultat obtenu est disponible de manière synchrone, il contient également deux champs alors qu'un Task<TResult> en tant que type de référence est un champ unique. Cela signifie qu'un appel de méthode retourne deux champs de données au lieu d'un, ce qui représente davantage de données à copier. Cela signifie également que si une méthode renvoyant l'un de ces éléments est attendue dans une méthode async, la machine à états de cette méthode async sera plus grande en raison de la nécessité de stocker la structure composée de deux champs au lieu d'un. référence unique.

En outre, pour des utilisations autres que la consommation du résultat d'une opération asynchrone via await, ValueTask<TResult>, il peut en résulter un modèle de programmation plus complexe, pouvant à son tour générer davantage d'attributions. Par exemple, considérons une méthode qui pourrait renvoyer un Task<TResult> avec une tâche mise en cache comme résultat courant ou un ValueTask<TResult>. Si le consommateur du résultat veut l'utiliser comme Task<TResult>, par exemple avec des méthodes telles que Task.WhenAll et Task.WhenAny, le ValueTask<TResult> devra d'abord être converti. dans un Task<TResult> en utilisant AsTask, ce qui conduit à une allocation qui aurait été évitée si un Task<TResult> mis en cache avait été utilisé en premier lieu.

En tant que tel, le choix par défaut de toute méthode asynchrone devrait être de renvoyer un Task ou Task<TResult>. Si l'analyse des performances le justifie, il convient d'utiliser un ValueTask<TResult> au lieu de Task<TResult>.

199
Stephen Cleary

Cependant, je ne comprends toujours pas quel est le problème avec l'utilisation de ValueTask toujours

Les types de structures ne sont pas gratuits. Copier des structures plus grandes que la taille d'une référence peut être plus lent que copier une référence. Stocker des structures plus grandes qu'une référence nécessite plus de mémoire que stocker une référence. Les structures dont la taille est supérieure à 64 bits peuvent ne pas être enregistrées lorsqu'une référence peut être enregistrée. Les avantages d’une pression de collecte moindre ne peuvent dépasser les coûts.

Les problèmes de performance doivent être abordés avec une discipline d'ingénierie. Fixez-vous 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 effectuant des mesures en cours de route pour vous assurer que vos modifications apportent effectivement des améliorations.

pourquoi async/wait n'a pas été construit avec un type de valeur depuis le début.

await a été ajouté à C # longtemps après que le type Task<T> existe déjà. Il aurait été quelque peu pervers d’inventer un nouveau type alors qu’il en existait déjà un. Et await a effectué de nombreuses itérations de conception avant de choisir celle qui a été expédiée en 2012. Le parfait est l'ennemi du bien; Il est préférable d’expédier une solution qui fonctionne bien avec l’infrastructure existante, puis d’apporter des améliorations s’il ya demande des utilisateurs.

Je remarque é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 un compilateur ajoute des risques et une charge de test considérables. Lorsque les seules choses que vous pouvez retourner sont nulles ou une tâche, l'équipe de test n'a pas à envisager de scénario dans lequel un type absolument fou est renvoyé. Tester un compilateur signifie non seulement déterminer quels programmes les gens sont susceptibles d’écrire, mais également quels programmes sont possibles , car nous voulons que le compilateur compile tous les éléments. programmes juridiques, pas seulement tous les programmes sensibles. C'est cher.

Quelqu'un peut-il expliquer quand ValueTask échouerait à faire le travail?

Le but de la chose est une performance améliorée. Il ne fait pas le travail s'il ne pas mesurable et de manière significative améliorer les performances. Il n'y a aucune garantie que ce sera le cas.

87
Eric Lippert

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

ValueTask<T> est une union discriminante d'un T et d'un Task<T>, ce qui la libère de l'allocation pour que ReadAsync<T> renvoie de manière synchrone une valeur T dont il dispose (contrairement à Task.FromResult<T> , qui doit allouer une instance Task<T>). ValueTask<T> est en attente, de sorte que la plupart des instances seront indiscernables d'un Task<T>.

ValueTask, en tant que 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 avoir une interface avec une méthode de retour de tâche. Chaque classe implémentant cette interface doit renvoyer une tâche même s’il s’exécute de manière synchrone (éventuellement avec Task.FromResult). Bien sûr, vous pouvez avoir 2 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”.

Cela vous permet donc d'écrire une méthode asynchrone ou synchrone, plutôt que d'écrire une méthode par ailleurs identique pour chacune. Vous pouvez l'utiliser n'importe où vous utilisez Task<T> mais cela ne signifie souvent rien ajouter.

Cela ajoute une chose: cela ajoute une promesse implicite à l'appelant que la méthode utilise réellement les fonctionnalités supplémentaires fournies par ValueTask<T>. Personnellement, je préfère choisir les types de paramètre et de retour qui en disent le plus possible à l'appelant. Ne retournez pas IList<T> si l'énumération ne peut pas fournir de compte; ne retournez pas IEnumerable<T> s'il le peut. Vos consommateurs ne devraient consulter aucune documentation pour savoir laquelle de vos méthodes peut raisonnablement être appelée de manière synchrone ou non.

Je ne vois pas dans les modifications futures de la conception un argument convaincant. Bien au contraire: si une méthode change de sémantique, elle devrait interrompre la construction jusqu'à ce que tous ses 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 la construction), envisagez le versionnage d'interface.

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

Si certains programmeurs qui conçoivent des méthodes asynchrones dans votre boutique ne sont pas en mesure de prendre des décisions éclairées, il peut être utile d'affecter un mentor principal à chacun de ces programmeurs moins expérimentés et de procéder à une révision hebdomadaire du code. S'ils devinent mal, expliquez pourquoi cela devrait être fait différemment. Il en va de même pour les joueurs seniors, mais cela mettra les juniors au courant beaucoup plus rapidement que de simplement les jeter dans le vif du sujet et de leur donner 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 écrivant des méthodes asynchrones, ces mêmes personnes les appellent-ils également? Sont-ils qualifiés pour déterminer par eux-mêmes lesquels sont sûrs d'appeler async, ou vont-ils commencer à appliquer une règle similaire arbitraire à la façon dont ils appellent ces choses?

Le problème ici n’est pas votre type de retour, ce sont les programmeurs qui se voient confier des rôles pour lesquels ils ne sont pas prêts. Cela a dû se produire pour une raison, alors je suis sûr que ce ne peut être facile à régler. Décrire ce n'est certainement pas une solution. Mais rechercher un moyen de résoudre le problème au-delà du compilateur n'est pas non plus une solution.

22
Ed Plunkett

Il y a quelques changements dans .Net Core 2.1 . À partir de .net core 2.1, ValueTask peut représenter non seulement les actions terminées synchrones, mais également les opérations asynchrones. De plus, nous recevons le type ValueTask non générique.

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

Nous avons encore besoin de formaliser les instructions, mais je m'attends à ce qu'il en soit de même pour la surface des API publiques:

  • La tâche offre le plus de convivialité.

  • ValueTask offre le plus d'options d'optimisation des performances.

  • Si vous écrivez une méthode virtuelle d’interface que d’autres remplaceront, ValueTask est le bon choix par défaut.
  • Si vous pensez que l'API sera utilisée sur des chemins d'accès chauds où les allocations seront importantes, ValueTask est un bon choix.
  • Sinon, lorsque les performances ne sont pas critiques, passez à Task, car il offre de meilleures garanties et facilité d'utilisation.

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

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

13
unsafePtr