0Pricing
C# Academy · Lekcja

ValueTask i unikanie alokacji

Używaj ValueTask do ograniczania alokacji na stercie w często wykonywanych ścieżkach, poznaj jego ograniczenia i unikaj typowych błędów użycia.

ValueTask i unikanie alokacji to bezpłatna lekcja C# Academy na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej C# Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs C# Academy zawiera 4 lekcji w sumie.

Problem alokacji związany z Task

Task<T> jest typem referencyjnym — każda metoda async alokuje obiekt Task na stercie, nawet jeśli wynik jest dostępny synchronicznie (na przykład z pamięci podręcznej). W często wykonywanych ścieżkach może to powodować znaczną presję na GC. Rozwiązaniem tego problemu jest ValueTask<T>.

Czym jest ValueTask?

ValueTask<T> jest strukturą, która może reprezentować zarówno wynik dostępny synchronicznie, jak i oczekującą operację asynchroniczną. Gdy wynik jest synchroniczny, alokacja na stercie nie jest potrzebna.

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

Zwracanie ValueTask z interfejsu

ValueTask<T> należy stosować w metodach interfejsu, które są często wywoływane i często kończą działanie synchronicznie. Konsumenci korzystają z nich dokładnie tak samo jak z 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 z IValueTaskSource

Aby uzyskać ekstremalną wydajność, należy zaimplementować IValueTaskSource<T> w celu buforowania obiektu przechowującego stan. Eliminuje to wszystkie alokacje, nawet w przypadku zakończeń asynchronicznych — rozwiązanie to jest używane wewnętrznie przez kod gniazd i operacji wejścia-wyjścia platformy .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

Ograniczenia ValueTask: await tylko raz

Najważniejsza zasada: na obiekcie ValueTask<T> można wykonać await najwyżej raz i nie można wywołać .Result przed jego ukończeniem. Naruszenie tej zasady powoduje niezdefiniowane zachowanie.

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

ValueTask<T> a Task<T> — przewodnik po wyborze

ValueTask<T> należy stosować tylko wtedy, gdy profilowanie wykaże presję alokacji powodowaną przez metody asynchroniczne. Nie należy używać go wszędzie — w kodzie wykonywanym rzadko zwiększa złożoność bez żadnych korzyści.

// 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 bez typu generycznego

ValueTask (bez typu generycznego) sprawdza się w metodach zwracających Task (asynchronicznych metodach bez wartości zwracanej). Należy używać go w często wywoływanych metodach, które często kończą działanie synchronicznie.

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

Pomiar alokacji za pomocą BenchmarkDotNet

Należy użyć [MemoryDiagnoser] w BenchmarkDotNet, aby zmierzyć alokacje przypadające na operację. To jedyny wiarygodny sposób sprawdzenia, czy ValueTask rzeczywiście pomaga.

[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 w SocketsHttpHandler

Stos HTTP platformy .NET intensywnie korzysta wewnętrznie z ValueTask. Zrozumienie tego mechanizmu pomaga pisać wysokowydajne klienty HTTP zgodne ze strategią alokacji środowiska uruchomieniowego.

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

Praktyczny przykład: usługa pamięci podręcznej na gorącej ścieżce

Usługa pamięci podręcznej, w której każde wywołanie może zakończyć się trafieniem do pamięci podręcznej — ValueTask eliminuje alokacje na gorącej ścieżce, a jednocześnie obsługuje asynchroniczny mechanizm awaryjny w przypadku braku danych w pamięci podręcznej.

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

Szybkie sprawdzenie

Jaka jest najważniejsza zasada dotycząca wykonywania await na ValueTask?

Podsumowanie: ValueTask i unikanie alokacji

Najważniejsze wnioski:

  • ValueTask<T>: struktura, która unika alokacji na stercie, gdy wynik jest synchroniczny
  • Najlepiej sprawdza się na gorących ścieżkach, które często kończą działanie synchronicznie (trafienia z pamięci podręcznej)
  • NIGDY nie należy wykonywać await na ValueTask<T> więcej niż raz — do współdzielenia należy użyć .AsTask()
  • Należy użyć [MemoryDiagnoser], aby przed optymalizacją zweryfikować oszczędności alokacji
  • Do ogólnego zastosowania należy preferować Task<T>; ValueTask<T> należy stosować tylko wtedy, gdy uzasadnia to profilowanie

Często zadawane pytania

Czy lekcja „ValueTask i unikanie alokacji” jest bezpłatna?

Tak — pełny tekst „ValueTask i unikanie alokacji” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu C# Academy, przejdź na CoddyKit PRO. Kurs C# Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „ValueTask i unikanie alokacji”?

Używaj ValueTask do ograniczania alokacji na stercie w często wykonywanych ścieżkach, poznaj jego ograniczenia i unikaj typowych błędów użycia. Ćwiczysz C# Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć C# Academy?

Nie wymagamy żadnego doświadczenia. C# Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „ValueTask i unikanie alokacji”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji C# Academy?

Tak. Każda lekcja C# Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. IAsyncEnumerable i await foreach
  2. System.Threading.Channels
  3. ValueTask i unikanie alokacji
  4. ConfigureAwait i kontekst synchronizacji
← Powrót do C# Academy