C# Academy · Les

ValueTask en allocaties vermijden

Gebruik ValueTask om heapallocaties in kritieke codepaden te verminderen, krijg inzicht in de beperkingen ervan en vermijd veelvoorkomende misbruikscenario's.

Les 3 van 412 stappen

ValueTask en allocaties vermijden is een gratis C# Academy-les op CoddyKit. Dit is les 3 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject C# Academy. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus C# Academy bevat in totaal 4 lessen.

Het allocatieprobleem bij Task

Task<T> is een referentietype — elke async-methode alloceert een Task op de heap, zelfs als het resultaat synchroon beschikbaar is (bijvoorbeeld uit een cache). In veelgebruikte codepaden kan dit aanzienlijke druk op de garbagecollector veroorzaken. ValueTask<T> lost dit op.

Wat is ValueTask?

ValueTask<T> is een struct die een synchroon beschikbaar resultaat of een nog wachtende asynchrone bewerking kan voorstellen. Wanneer het resultaat synchroon beschikbaar is, is geen allocatie op de heap nodig.

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

ValueTask retourneren vanuit een interface

Gebruik ValueTask<T> in interfacemethoden die vaak worden aangeroepen en vaak synchroon worden voltooid. Consumenten gebruiken het precies zoals 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 met IValueTaskSource

Implementeer voor extreme prestaties IValueTaskSource<T> om het onderliggende statusobject te poolen. Hiermee voorkom je alle allocaties, zelfs bij asynchrone voltooiingen — dit wordt intern gebruikt door de socket- en I/O-code van .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

Beperkingen van ValueTask: slechts één keer afwachten

Kritieke regel: een ValueTask<T> mag maximaal één keer worden afgewacht en je mag `.Result` niet aanroepen voordat de bewerking is voltooid. Als je dit toch doet, ontstaat ongedefinieerd gedrag.

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

Keuzehulp: ValueTask<T> versus Task<T>

Gebruik ValueTask<T> alleen wanneer profilering laat zien dat asynchrone methoden te veel allocatiedruk veroorzaken. Gebruik het niet overal — in code die weinig wordt aangeroepen, voegt het complexiteit toe zonder voordeel.

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

Niet-generieke ValueTask

ValueTask (niet-generiek) werkt voor methoden die Task retourneren (asynchrone methoden zonder retourwaarde). Gebruik het voor methoden die vaak worden aangeroepen en vaak synchroon worden voltooid.

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
    }
}

Allocaties meten met BenchmarkDotNet

Gebruik [MemoryDiagnoser] in BenchmarkDotNet om allocaties per bewerking te meten. Dit is de enige betrouwbare manier om te controleren of ValueTask daadwerkelijk helpt.

[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

De .NET HTTP-stack gebruikt intern op grote schaal ValueTask. Als je dit begrijpt, kun je hoogwaardige HTTP-clients schrijven die aansluiten bij de allocatiestrategie van de 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

Praktijkvoorbeeld: cacheservice voor veelgebruikte codepaden

Een cacheservice waarbij elke aanroep mogelijk een cachetreffer oplevert — ValueTask elimineert allocaties in het veelgebruikte codepad en ondersteunt tegelijkertijd een asynchrone fallback bij cachemissers.

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;
    }
}

Korte test

Wat is de kritieke regel voor het afwachten van een ValueTask?

Samenvatting: ValueTask en allocaties vermijden

Belangrijkste punten:

  • ValueTask<T>: struct die allocatie op de heap voorkomt wanneer het resultaat synchroon beschikbaar is
  • Het meest geschikt voor veelgebruikte codepaden die vaak synchroon worden voltooid (cachetreffers)
  • Wacht een ValueTask<T> NOOIT meer dan één keer af — gebruik .AsTask() om de bewerking te delen
  • Gebruik [MemoryDiagnoser] om de besparing op allocaties te controleren voordat je optimaliseert
  • Geef voor algemeen gebruik de voorkeur aan Task<T>; gebruik ValueTask<T> alleen wanneer profilering dit rechtvaardigt
Gratis beginnen

Leer C# met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
93
Lessen
346

Veelgestelde vragen

Is de les “ValueTask en allocaties vermijden” gratis?

Ja — de volledige tekst van “ValueTask en allocaties vermijden” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus C# Academy wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus C# Academy bevat in totaal 4 lessen.

Wat leer ik in “ValueTask en allocaties vermijden”?

Gebruik ValueTask om heapallocaties in kritieke codepaden te verminderen, krijg inzicht in de beperkingen ervan en vermijd veelvoorkomende misbruikscenario's. Je oefent met C# Academy door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met C# Academy te beginnen?

Ervaring vooraf is niet nodig. C# Academy op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.

Hoe lang duurt de les “ValueTask en allocaties vermijden”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over C# Academy?

Ja. Elke les over C# Academy bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. IAsyncEnumerable en await foreach
  2. System.Threading.Channels
  3. ValueTask en allocaties vermijden
  4. ConfigureAwait en SynchronizationContext
← Terug naar C# Academy