CodeGym /Corsi /C# SELF /Ottimizzazione delle prestazioni:

Ottimizzazione delle prestazioni: ValueTask

C# SELF
Livello 62 , Lezione 2
Disponibile

1. Introduzione

Prima di tuffarci nel codice, facciamolo semplice. Ricordate che creare un oggetto in .NET (e ancor di più — un task!) costa memoria e un po' di tempo CPU? Ora immaginate un'API asincrona che nel 50% dei casi restituisce il risultato istantaneamente (per esempio lo prende dalla cache), e nell'altro 50% consulta il database, quindi l'operazione diventa realmente asincrona e richiede un Task. Succede spesso — per esempio in cache di file, API di rete, pool di oggetti e altri scenari "asincroni, ma a volte istantanei".

Se restituiamo sempre un Task, anche per risultati istantanei dobbiamo creare oggetti inutili. E se potessimo restituire il risultato senza il task quando è già pronto? Proprio per questo è nato ValueTask.

Fatto: i soliti Task.CompletedTask e Task.FromResult(…) risparmiano tempo di esecuzione, ma creano oggetti condivisi che potrebbero non essere ideali per scenari ad alto carico.

Cos'è ValueTask

ValueTask è una struct-wrapper speciale che può rappresentare o un risultato già pronto, o (se l'operazione è veramente asincrona) il task stesso. In parole povere, è un "pacchetto" che può contenere o solo il valore oppure un riferimento a un Task.

Ci sono due varianti principali:

  • ValueTask — "senza valore" (non restituisce un valore, come Task)
  • ValueTask<TResult> — wrapper per un valore (analogo a Task<TResult>)

Confronto tra Task e ValueTask

Tipo Numero di allocazioni Può essere completato sincronicamente Usi tipici
Task
Una (heap) Sì/No Quasi sempre
ValueTask
Zero/uno Sì/No Ottimizzazione
ValueTask<T>
Zero/uno Sì/No Ottimizzazione

Quando usare ValueTask

C'è una regola d'oro: se scrivi un metodo asincrono che restituisce sempre il risultato solo dopo un'operazione async, usa Task. È semplice, sicuro e comprensibile a tutti.

Usa ValueTask quando:

  • Il risultato può essere ottenuto sincronicamente (per esempio dalla cache, da un pool, dalla memoria), e vuoi risparmiare sulle allocazioni inutili.
  • L'operazione asincrona capita non troppo spesso (altrimenti i benefici si perdono a fronte della complessità del codice e della copia delle struct).

Attenzione! Se restituisci sempre un risultato asincrono — usa Task. Se vuoi che la tua API sia "super-ottimale" per risultati istantanei frequenti — allora ValueTask.

2. Risultato sincrono o asincrono

Consideriamo una funzione che cerca un utente per nome. Se l'utente è in cache — lo restituiamo immediatamente; se no — lo carichiamo asincronicamente dal "database":

// Modelliamo il nostro utente
public class User
{
    public string Name { get; set; }
}

// La nostra cache (molto semplice)
private readonly Dictionary<string, User> _localCache = new();

public async ValueTask<User> FindUserAsync(string name)
{
    // Controlliamo la cache locale
    if (_localCache.TryGetValue(name, out var user))
    {
        // Risultato immediato — nessuna allocazione di task!
        return user;
    }

    // Qui una presunta lunga operazione asincrona (per esempio dal DB)
    user = await LoadUserFromDbAsync(name);
    // Mettiamo in cache per il futuro
    _localCache[name] = user;
    return user;
}

private async Task<User> LoadUserFromDbAsync(string name)
{
    // Simuliamo una latenza
    await Task.Delay(500);
    return new User { Name = name };
}

Importante: In caso di hit nella cache restituiamo il risultato normalmente — nessuna allocazione di task! Solo quando dobbiamo "recuperare" il risultato creiamo davvero un'operazione asincrona.

Com'è fatto internamente ValueTask

Dentro a ValueTask può esserci o un valore pronto, o un riferimento a un Task:

ValueTask result = ValueTask.CompletedTask;
ValueTask<int> valueResult = new ValueTask<int>(42);
ValueTask<int> valueResult2 = new ValueTask<int>(Task.Run(() => 42));

Quando usi await il compilatore capisce da solo: se il risultato è immediato non creerà oggetti extra.

Importante su await e ValueTask

I metodi async vanno d'accordo con await per ValueTask:

public async ValueTask PingAsync()
{
    // ...
    await Task.Delay(10);
}

Ma se salvi un'istanza di ValueTask e decidi di attenderla più tardi, ricorda: non si può fare await sulla stessa istanza più di una volta. Un Task può essere atteso molte volte, un ValueTask no.

Errore: await ripetuto su ValueTask

ValueTask<int> task = ComputeAsync();

// Questo va bene
int a = await task;

// Questo è un errore! Un secondo await sulla stessa istanza di ValueTask non è permesso:
// int b = await task; // NON SI PUÒ!

3. Riscriviamo parte della nostra applicazione console

Supponiamo di sviluppare un mini-reader di libri: parte dei testi è già caricata nella cache, il resto viene scaricato asincronicamente. Implementiamo un metodo ottimizzato per ottenere la prima riga di un libro usando ValueTask:

private readonly Dictionary<string, string> _bookCache = new();

public async ValueTask<string> GetFirstLineOfBookAsync(string title)
{
    if (_bookCache.TryGetValue(title, out var bookText))
    {
        var firstLine = bookText.Split('\n')[0];
        return firstLine;
    }

    // Supponiamo il download asincrono del libro
    var downloadedBook = await DownloadBookTextAsync(title);
    _bookCache[title] = downloadedBook;
    return downloadedBook.Split('\n')[0];
}

private async Task<string> DownloadBookTextAsync(string title)
{
    // Simuliamo una latenza (per esempio scaricamento da internet)
    await Task.Delay(1000);
    return $"Book: {title}\nThis is the first line.\nSecond line...";
}

Così ValueTask risparmia la creazione di un task quando il libro è già in cache.

4. Dettagli utili

Segnale: quando NON usare ValueTask

Se la tua API è sempre asincrona (per esempio sempre comunicazione di rete) — usa Task, è più semplice e sicuro.

Come convertire ValueTask in Task e viceversa

Capita che un ValueTask non si adatti a un'API che vuole Task. Usa .AsTask():

ValueTask<int> valueTask = ComputeAsync();
Task<int> task = valueTask.AsTask();

Se devi trasformare un Task in ValueTask — crea semplicemente un ValueTask dal task:

Task<int> task = ComputeAsyncTask();
ValueTask<int> valueTask = new ValueTask<int>(task);

Confronto: ValueTask vs Task

Caratteristica
Task
ValueTask
Await ripetuto Permesso Non permesso
Allocazioni per risposte veloci Ci sono No (se il risultato è immediato)
Compatibilità dell'interfaccia Ampia supportata Richiede wrapper aggiuntivi
Applicabilità Ovunque Solo per ottimizzazione
Pooling (Pool) Usa pool comune No, è struct
Semplicità Semplice Più complesso

Uso di ValueTask nella vita reale

public async ValueTask<string> GetMessageAsync(int id)
{
    if (_messageCache.TryGetValue(id, out var value))
        return value;

    var result = await LoadMessageFromDbAsync(id);
    _messageCache[id] = result;
    return result;
}

Da dire in un colloquio: Se ti chiedono come migliorare ulteriormente le prestazioni di un'API asincrona con caching, parla di ValueTask.

Esempi di compatibilità con LINQ e IAsyncEnumerable<T>

Se vuoi usare ValueTask con LINQ asincrono (IAsyncEnumerable<T>), è supportato in .NET (per esempio il metodo ToListAsync):

public async ValueTask<List<int>> GetPrimeNumbersAsync()
{
    // Simuliamo che parte dei numeri sia già calcolata, e parte sia risultato di operazioni asincrone
    // ... (esempio omesso per brevità)
    return new List<int> { 2, 3, 5, 7, 11 };
}

5. Conclusioni e caratteristiche dell'implementazione

  1. Usa ValueTask per ottimizzare quando una parte significativa delle volte il risultato è pronto immediatamente (per esempio dalla cache).
  2. Non usare ValueTask "perché sì" — complicherebbe il codice e causerebbe errori.
  3. Non fare await su un ValueTask più volte. Se serve, convertilo in Task.
  4. Tutta la compatibilità con Task passa tramite .AsTask().
  5. Ricorda che ValueTask è una struct, e può comportarsi diversamente quando viene copiato.

6. Errori tipici quando si lavora con ValueTask

Errore №1: await ripetuto su una stessa istanza di ValueTask. ValueTask è una struct, non può essere await-ata due volte. Un secondo await porterà a eccezioni o comportamenti errati.

Errore №2: usare ValueTask senza verificare la compatibilità. Alcune API esterne richiedono Task, e passare un ValueTask direttamente può creare problemi.

Errore №3: usare ValueTask senza motivo. Se il risultato è sempre asincrono, usare ValueTask complica il codice senza beneficio reale.

Errore №4: copia della struct ValueTask. Una struct copiata può comportarsi inaspettatamente con await, è importante lavorare con l'originale.

2
Compito
C# SELF, livello 62, lezione 2
Bloccato
Restituire il risultato istantaneamente o in modo asincrono
Restituire il risultato istantaneamente o in modo asincrono
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION