0Pricing
C# Academy · Lezione

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 needed

Restituire 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 throughput

Vincoli 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 times

Guida 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 it

ValueTask 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 hit

ValueTask 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/O

Caso 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; utilizzare ValueTask<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

  1. IAsyncEnumerable e await foreach
  2. System.Threading.Channels
  3. ValueTask ed evitare allocazioni
  4. ConfigureAwait e contesto di sincronizzazione
← Torna a C# Academy