ValueTask ed evitare allocazioni
Usi ValueTask per ridurre le allocazioni nell'heap nei percorsi più eseguiti, ne comprenda i vincoli ed eviti gli errori d'uso più comuni.
ValueTask ed evitare allocazioni è una lezione C# Academy gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento C# Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso C# Academy include 4 lezioni in totale.
Il problema delle allocazioni con Task
Task<T> è un tipo riferimento: ogni metodo async alloca un Task nell'heap, anche se il risultato è disponibile in modo sincrono (ad esempio, da una cache). Nei percorsi più eseguiti questo può causare una notevole pressione sul GC. ValueTask<T> risolve il problema.
Che cos'è ValueTask?
ValueTask<T> è una struct che può rappresentare sia un risultato disponibile in modo sincrono sia un'operazione asincrona in attesa. Quando il risultato è sincrono, non è necessaria alcuna allocazione nell'heap.
// Task<T>: always allocates, even when result is cached
public async Task<string> GetCached_Task() =>
_cache.TryGetValue("key", out var v) ? v : await FetchAsync();
// ValueTask<T>: zero allocation when value is cached
public ValueTask<string> GetCached_ValueTask() =>
_cache.TryGetValue("key", out var v)
? ValueTask.FromResult(v) // no allocation
: new ValueTask<string>(FetchAsync()); // allocation only when neededRestituire ValueTask da un'interfaccia
Utilizzi ValueTask<T> nei metodi di interfaccia chiamati frequentemente e che spesso terminano in modo sincrono. I consumatori lo utilizzano esattamente come Task<T>.
public interface IProductRepository
{
// ValueTask: often completes synchronously from cache
ValueTask<Product?> GetByIdAsync(int id);
// Task: always async (DB query)
Task<List<Product>> SearchAsync(string query);
}
// Consumption is identical to Task:
var product = await _repo.GetByIdAsync(42);
var results = await _repo.SearchAsync("keyboard");ValueTask con IValueTaskSource
Per prestazioni estreme, implementi IValueTaskSource<T> per utilizzare un pool per l'oggetto di stato sottostante. In questo modo evita tutte le allocazioni anche per i completamenti asincroni; questa tecnica viene utilizzata internamente dal codice socket e I/O di .NET.
// In most cases, use the simple ValueTask approach.
// IValueTaskSource is for library authors needing zero-alloc async at extreme scale.
// Example: System.IO.Pipelines uses this internally
var result = await pipe.Reader.ReadAsync(ct);
// ReadAsync returns ValueTask<ReadResult> backed by IValueTaskSource
// -- zero allocation for every read even under high throughputVincoli di ValueTask: attenderlo una sola volta
Regola fondamentale: un ValueTask<T> deve essere atteso al massimo una volta e non è possibile chiamare .Result prima del suo completamento. La violazione di questa regola causa un comportamento non definito.
var vt = repo.GetByIdAsync(42);
// BAD: await multiple times
var a = await vt; // OK
var b = await vt; // UNDEFINED BEHAVIOR — don't do this!
// BAD: two concurrent awaits
var t1 = vt.AsTask(); // converts to Task (safe to share)
var t2 = vt.AsTask(); // second AsTask() on same ValueTask — WRONG
// GOOD: if you need multiple awaits, convert to Task
var task = repo.GetByIdAsync(42).AsTask();
var r1 = await task;
var r2 = await task; // OK — Task can be awaited multiple timesGuida alla scelta tra ValueTask<T> e Task<T>
Utilizzi ValueTask<T> solo quando il profiling mostra una pressione sulle allocazioni causata dai metodi asincroni. Non lo utilizzi ovunque: aggiunge complessità senza apportare vantaggi nel codice eseguito raramente.
// USE ValueTask when:
// 1. The method often completes synchronously (cache hit)
// 2. The method is on a hot path called millions of times/sec
// 3. You're writing a library with high-throughput I/O
// USE Task when:
// 1. The result is always async
// 2. The method might be awaited multiple times
// 3. You use Task.WhenAll / Task.WhenAny
// 4. You store the task in a variable and share itValueTask non generico
ValueTask (non generico) è adatto ai metodi che restituiscono Task (asincroni senza valore restituito). Lo utilizzi per i metodi chiamati frequentemente che spesso terminano in modo sincrono.
public interface ICache
{
// Often a no-op when value exists — ValueTask avoids allocation
ValueTask SetAsync(string key, string value);
ValueTask InvalidateAsync(string key);
}
public class InMemoryCache : ICache
{
private readonly Dictionary<string, string> _data = new();
public ValueTask SetAsync(string key, string value)
{
_data[key] = value;
return ValueTask.CompletedTask; // no allocation
}
}Misurare le allocazioni con BenchmarkDotNet
Utilizzi [MemoryDiagnoser] in BenchmarkDotNet per misurare le allocazioni per operazione. Questo è l'unico modo affidabile per verificare che ValueTask sia effettivamente utile.
[MemoryDiagnoser]
public class CacheBenchmark
{
private readonly CachedRepo _repo = new();
[Benchmark]
public async Task<Product?> Task_GetById()
=> await _repo.GetByIdTask(1);
[Benchmark]
public async ValueTask<Product?> ValueTask_GetById()
=> await _repo.GetByIdValueTask(1);
}
// Run: dotnet run -c Release
// Compare Gen0 allocations column — ValueTask should show 0B on cache hitValueTask in SocketsHttpHandler
Lo stack HTTP di .NET utilizza ampiamente ValueTask al suo interno. Comprendere questo meccanismo aiuta a scrivere client HTTP ad alte prestazioni, in linea con la strategia di allocazione del runtime.
// HttpClient uses ValueTask-based sockets internally.
// When using HttpClient.GetAsync(), the response is awaited with Task<HttpResponseMessage>.
// For streaming large bodies, use:
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
await using var stream = await response.Content.ReadAsStreamAsync();
// Stream reads internally use ValueTask<int> for zero-allocation I/OCaso reale: servizio cache per un percorso ad alta frequenza
Un servizio cache in cui ogni chiamata può potenzialmente essere un cache hit: ValueTask elimina le allocazioni nel percorso più frequente, mantenendo il supporto all'alternativa asincrona per i cache miss.
public class ProductCache
{
private readonly Dictionary<int, Product> _cache = new();
private readonly IProductRepository _repo;
public ProductCache(IProductRepository repo) => _repo = repo;
public ValueTask<Product?> GetAsync(int id)
{
// Cache hit: no allocation
if (_cache.TryGetValue(id, out var cached))
return ValueTask.FromResult<Product?>(cached);
// Cache miss: async fetch
return new ValueTask<Product?>(FetchAndCacheAsync(id));
}
private async Task<Product?> FetchAndCacheAsync(int id)
{
var product = await _repo.GetByIdAsync(id);
if (product is not null) _cache[id] = product;
return product;
}
}Controllo rapido
Qual è la regola fondamentale per l'await di un ValueTask
Riepilogo: ValueTask ed eliminazione delle allocazioni
Punti chiave:
ValueTask<T>: struct che evita l'allocazione nell'heap quando il risultato è sincrono- Ideale per i percorsi più eseguiti che terminano spesso in modo sincrono (cache hit)
- NON attendere mai un
ValueTask<T>più di una volta — utilizzare.AsTask()per condividerlo - Utilizzare
[MemoryDiagnoser]per verificare il risparmio di allocazioni prima di ottimizzare - Preferire
Task<T>per l'uso generale; utilizzareValueTask<T>solo quando il profiling lo giustifica
Domande Frequenti
La lezione «ValueTask ed evitare allocazioni» è gratuita?
Sì — il testo completo di «ValueTask ed evitare allocazioni» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso C# Academy, passa a CoddyKit PRO. Il corso C# Academy include 4 lezioni in totale.
Cosa imparerò in «ValueTask ed evitare allocazioni»?
Usi ValueTask per ridurre le allocazioni nell'heap nei percorsi più eseguiti, ne comprenda i vincoli ed eviti gli errori d'uso più comuni. Eserciti C# Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare C# Academy?
Non è richiesta alcuna esperienza precedente. C# Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.
Quanto tempo richiede la lezione «ValueTask ed evitare allocazioni»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione C# Academy?
Sì. Ogni lezione C# Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- IAsyncEnumerable e await foreach
- System.Threading.Channels
- ValueTask ed evitare allocazioni
- ConfigureAwait e contesto di sincronizzazione