Perché utilizzare un'attività <T> su ValueTask <T> in C #?


168

A partire da C # 7.0, i metodi asincroni possono restituire ValueTask <T>. La spiegazione dice che dovrebbe essere usato quando abbiamo un risultato memorizzato nella cache o simulando asincrono tramite codice sincrono. Tuttavia, non capisco ancora qual è il problema con l'utilizzo di ValueTask sempre o in realtà perché async / waitit non è stato creato con un tipo di valore dall'inizio. Quando ValueTask non farebbe il lavoro?


7
Ho il sospetto che abbia a che fare con i vantaggi ValueTask<T>(in termini di allocazioni) di non materializzarsi per operazioni che sono effettivamente asincrone (perché in quel caso ValueTask<T>avrà ancora bisogno di allocazione di heap). C'è anche la questione di Task<T>avere molto altro supporto all'interno delle biblioteche.
Jon Skeet,

3
Le librerie esistenti di @JonSkeet sono un problema, ma ciò pone la domanda se Task fosse stato ValueTask dall'inizio? I vantaggi potrebbero non esistere quando lo si utilizza per roba vera asincrona ma è dannoso?
Stilgar

8
Vedi github.com/dotnet/corefx/issues/4708#issuecomment-160658188 per più saggezza di quanto sarei in grado di trasmettere :)
Jon Skeet


3
@JoelMueller la trama si infittisce :)
Stilgar

Risposte:


245

Dai documenti API (enfasi aggiunta):

I metodi possono restituire un'istanza di questo tipo di valore quando è probabile che il risultato delle loro operazioni sia disponibile in modo sincrono e quando si prevede che il metodo verrà invocato così frequentemente che il costo di allocazione di un nuovo Task<TResult>per ogni chiamata sarà proibitivo.

Ci sono compromessi nell'uso di a ValueTask<TResult>invece di a Task<TResult>. Ad esempio, mentre a ValueTask<TResult>può aiutare a evitare un'allocazione nel caso in cui il risultato positivo sia disponibile in modo sincrono, contiene anche due campi mentre un Task<TResult>tipo di riferimento è un campo singolo. Ciò significa che una chiamata di metodo finisce per restituire due campi di dati invece di uno, che sono più dati da copiare. Significa anche che se un metodo che restituisce uno di questi è atteso all'interno di un asyncmetodo, la macchina a stati per quel asyncmetodo sarà più grande a causa della necessità di memorizzare la struttura che è due campi invece di un singolo riferimento.

Inoltre, per usi diversi dal consumare il risultato di un'operazione asincrona tramite await, ValueTask<TResult>può portare a un modello di programmazione più contorto, che a sua volta può effettivamente portare a più allocazioni. Ad esempio, prendere in considerazione un metodo che potrebbe restituire a Task<TResult>con un'attività memorizzata nella cache come risultato comune o a ValueTask<TResult>. Se il consumatore del risultato desidera utilizzarlo come a Task<TResult>, ad esempio con metodi come Task.WhenAlle Task.WhenAny, il ValueTask<TResult>primo dovrebbe essere convertito in un Task<TResult>utilizzo AsTask, il che porta a un'allocazione che sarebbe stata evitata se Task<TResult>fosse stata utilizzata una cache innanzitutto.

Pertanto, la scelta predefinita per qualsiasi metodo asincrono dovrebbe essere quella di restituire un Tasko Task<TResult>. Solo se l'analisi delle prestazioni dimostra che vale la pena ValueTask<TResult>utilizzare invece di Task<TResult>.


7
@MattThomas: consente di risparmiare una singola Taskallocazione (che al giorno d'oggi è piccola ed economica), ma a costo di aumentare l' allocazione esistente del chiamante e raddoppiare la dimensione del valore di ritorno (influendo sull'allocazione del registro). Sebbene sia una scelta chiara per uno scenario di lettura con buffer, applicarlo di default a tutte le interfacce non è qualcosa che consiglierei.
Stephen Cleary,

1
A destra, Tasko ValueTaskpuò essere usato come tipo di ritorno sincrono (con Task.FromResult). Ma c'è ancora valore (eh) ValueTaskse hai qualcosa che ti aspetti di essere sincrono. ReadByteAsyncessere un classico esempio. Credo che siaValueTask stato creato principalmente per i nuovi "canali" (flussi di byte di basso livello), eventualmente utilizzati anche nel core ASP.NET dove le prestazioni contano davvero.
Stephen Cleary,

1
Oh, lo so che lol, mi chiedevo solo se avessi qualcosa da aggiungere a quel commento specifico;)
julealgon

2
Questo PR commuta il saldo preferendo ValueTask? (rif: blog.marcgravell.com/2019/08/… )
stuartd

2
@stuartd: per ora, consiglierei comunque di usarlo Task<T>come predefinito. Questo solo perché la maggior parte degli sviluppatori non ha familiarità con le restrizioni ValueTask<T>(in particolare, la regola "consuma una volta sola" e la regola "nessun blocco"). Detto questo, se tutti gli sviluppatori della tua squadra sono bravi ValueTask<T>, allora consiglierei una linea guida di preferenza a livello di squadraValueTask<T> .
Stephen Cleary,

104

Tuttavia, non capisco ancora qual è il problema con l'utilizzo di ValueTask sempre

I tipi di strutture non sono gratuiti. La copia di strutture più grandi della dimensione di un riferimento può essere più lenta della copia di un riferimento. La memorizzazione di strutture più grandi di un riferimento richiede più memoria rispetto alla memorizzazione di un riferimento. Le strutture più grandi di 64 bit potrebbero non essere registrate quando è possibile registrare un riferimento. I vantaggi di una pressione di raccolta più bassa non possono superare i costi.

I problemi di prestazione dovrebbero essere affrontati con una disciplina ingegneristica. Definisci obiettivi, misura i tuoi progressi rispetto agli obiettivi, quindi decidi come modificare il programma se gli obiettivi non vengono raggiunti, misurando lungo il percorso per assicurarti che le tue modifiche siano effettivamente miglioramenti.

perché async / await non è stato creato con un tipo di valore dall'inizio.

awaitè stato aggiunto a C # molto tempo dopo che il Task<T>tipo esisteva già. Sarebbe stato un po 'perverso inventare un nuovo tipo quando ne esisteva già uno. E awaitha attraversato molte iterazioni di design prima di stabilirsi su quella che è stata spedita nel 2012. Il perfetto è nemico del bene; meglio fornire una soluzione che funzioni bene con l'infrastruttura esistente e, in caso di richiesta dell'utente, fornire miglioramenti in un secondo momento.

Noto anche che la nuova funzionalità di consentire ai tipi forniti dall'utente di essere l'output di un metodo generato dal compilatore aggiunge un considerevole rischio e onere di prova. Quando le uniche cose che puoi restituire sono nulle o un'attività, il team di test non deve considerare nessuno scenario in cui viene restituito un tipo assolutamente pazzo. Testare un compilatore significa capire non solo quali programmi la gente probabilmente scriverà, ma quali programmi è possibile scrivere, perché vogliamo che il compilatore compili tutti i programmi legali, non solo tutti i programmi sensibili. È caro.

Qualcuno può spiegare quando ValueTask non riuscirebbe a fare il lavoro?

Lo scopo è migliorare le prestazioni. Non fa il lavoro se non migliora in modo misurabile e significativo le prestazioni. Non vi è alcuna garanzia che lo farà.


23

ValueTask<T>non è un sottoinsieme di Task<T>, è un superset .

ValueTask<T>è un'unione discriminata di T e a Task<T>, che lo rende privo di allocazione per ReadAsync<T>restituire in modo sincrono un valore T che ha a disposizione (al contrario dell'uso Task.FromResult<T>, che deve allocare Task<T>un'istanza). ValueTask<T>è attendibile, quindi la maggior parte del consumo di istanze sarà indistinguibile da a Task<T>.

ValueTask, essendo una struttura, consente di scrivere metodi asincroni che non allocano memoria quando vengono eseguiti in modo sincrono senza compromettere la coerenza dell'API. Immagina di avere un'interfaccia con un metodo di restituzione delle attività. Ogni classe che implementa questa interfaccia deve restituire un'attività anche se si esegue in modo sincrono (si spera utilizzando Task.FromResult). Ovviamente puoi avere 2 metodi diversi sull'interfaccia, uno sincrono e uno asincrono, ma ciò richiede 2 diverse implementazioni per evitare "sync over async" e "async over sync".

Quindi ti consente di scrivere un metodo che è asincrono o sincrono, piuttosto che scrivere un metodo altrimenti identico per ciascuno. Potresti usarlo ovunque tu usi Task<T>ma spesso non aggiungerebbe nulla.

Bene, aggiunge una cosa: aggiunge una promessa implicita al chiamante che il metodo effettivamente utilizza la funzionalità aggiuntiva che ValueTask<T>fornisce. Personalmente preferisco scegliere i parametri e restituire i tipi che dicono al chiamante il più possibile. Non restituire IList<T>se l'enumerazione non è in grado di fornire un conteggio; non tornare IEnumerable<T>se può. I tuoi consumatori non dovrebbero consultare alcuna documentazione per sapere quali dei tuoi metodi possono ragionevolmente essere chiamati in modo sincrono e quali no.

Non vedo i futuri cambiamenti di design come un argomento convincente lì. Al contrario: se un metodo cambia la sua semantica, dovrebbe interrompere la build fino a quando tutte le chiamate ad esso non vengono aggiornate di conseguenza. Se questo è considerato indesiderabile (e credetemi, sono solidale con il desiderio di non infrangere la build), prendete in considerazione il controllo delle versioni dell'interfaccia.

Questo è essenzialmente ciò che serve per scrivere forte.

Se alcuni programmatori che progettano metodi asincroni nel tuo negozio non sono in grado di prendere decisioni informate, potrebbe essere utile assegnare un mentore senior a ciascuno di quei programmatori meno esperti e avere una revisione settimanale del codice. Se indovinano male, spiega perché dovrebbe essere fatto diversamente. È un problema per i ragazzi più anziani, ma porterà i giovani a velocizzare molto più rapidamente che lanciarli nel profondo e dare loro una regola arbitraria da seguire.

Se l'uomo che ha scritto il metodo non sa se può essere chiamato in modo sincrono, chi sulla Terra lo fa ?!

Se hai così tanti programmatori inesperti che scrivono metodi asincroni, anche quelle stesse persone li chiamano? Sono qualificati per capire da soli quali sono sicuri di chiamare asincroni o inizieranno ad applicare una regola altrettanto arbitraria a come chiamano queste cose?

Il problema qui non sono i tuoi tipi di ritorno, ma i programmatori vengono assegnati a ruoli per i quali non sono pronti. Questo deve essere successo per un motivo, quindi sono sicuro che non può essere banale da risolvere. Descriverlo certamente non è una soluzione. Ma cercare un modo per intrufolarsi oltre il compilatore non è una soluzione.


4
Se vedo tornare un metodo ValueTask<T>, suppongo che il tizio che ha scritto il metodo lo abbia fatto perché il metodo utilizza effettivamente la funzionalità che ValueTask<T>aggiunge. Non capisco perché pensi che sia desiderabile che tutti i tuoi metodi abbiano lo stesso tipo restituito. Qual è l'obiettivo lì?
15ee8f99-57ff-4f92-890c-b56153

2
L'obiettivo 1 sarebbe quello di consentire una modifica dell'attuazione. Che cosa succede se non sto memorizzando nella cache il risultato ora, ma aggiungo il codice di memorizzazione nella cache in un secondo momento? L'obiettivo 2 sarebbe quello di avere una chiara linea guida per un team in cui non tutti i membri comprendono quando ValueTask è utile. Fallo sempre se non ci sono danni nell'usarlo.
Stilgar

6
A mio avviso, è quasi come avere tutti i tuoi metodi di ritorno objectperché forse un giorno vorrai che tornassero intinvece di string.
15ee8f99-57ff-4f92-890c-b56153

3
Forse, ma se poni la domanda "perché dovresti mai restituire una stringa anziché l'oggetto", posso facilmente rispondere indicando la mancanza di sicurezza del tipo. Le ragioni per non usare ValueTask ovunque sembrano essere molto più sottili.
Stilgar

3
@Stilgar Una cosa che direi: i problemi sottili dovrebbero preoccuparti più di quelli ovvi.
15ee8f99-57ff-4f92-890c-b56153

20

Ci sono alcune modifiche in .Net Core 2.1 . A partire da .net core 2.1 ValueTask può rappresentare non solo le azioni sincrone completate, ma anche quelle asincrone completate. Inoltre riceviamo un tipo non generico ValueTask.

Lascerò il commento di Stephen Toub relativo alla tua domanda:

Dobbiamo ancora formalizzare la guida, ma mi aspetto che sarà qualcosa del genere per la superficie dell'API pubblica:

  • L'attività fornisce la massima usabilità.

  • ValueTask offre la maggior parte delle opzioni per l'ottimizzazione delle prestazioni.

  • Se stai scrivendo un'interfaccia / metodo virtuale che altri sovrascriveranno, ValueTask è la scelta predefinita giusta.

  • Se si prevede che l'API venga utilizzata su percorsi caldi in cui le allocazioni contano, ValueTask è una buona scelta.

  • Altrimenti, laddove le prestazioni non siano critiche, l'impostazione predefinita è Task, in quanto fornisce migliori garanzie e usabilità.

Dal punto di vista dell'implementazione, molte delle istanze ValueTask restituite saranno comunque supportate da Task.

La funzione può essere utilizzata non solo nel .net core 2.1. Sarai in grado di usarlo con il pacchetto System.Threading.Tasks.Extensions .


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.