CodeGym /Kurse /C# SELF /Performance-Optimierung: ValueTask

Performance-Optimierung: ValueTask

C# SELF
Level 62, Lektion 2
Verfügbar

1. Einführung

Bevor wir in den Code springen, lass es uns einfach erklären. Erinnerst du dich, dass das einfache Erzeugen eines Objekts in .NET (und besonders eines Tasks!) Speicher kostet und etwas CPU-Zeit? Stell dir nun ein asynchrones API vor, das in der Hälfte der Fälle das Ergebnis sofort zurückgibt (z. B. aus dem Cache), und in der anderen Hälfte auf die Datenbank zugreift, wodurch die Operation wirklich asynchron wird und ein Task nötig ist. Das kommt oft vor — z. B. bei Dateicaches, Netzwerk-APIs, Objektpools und anderen "asynchronen, aber manchmal sofortigen" Szenarien.

Wenn wir immer ein Task zurückgeben, entstehen auch bei sofortigen Ergebnissen unnötige Objekte. Was wäre, wenn wir das Ergebnis ohne Task zurückgeben könnten, falls es schon fertig ist? Genau dafür wurde ValueTask erfunden.

Fakt: die Standard-Task.CompletedTask und Task.FromResult(…) sparen zwar Ausführungszeit, erzeugen aber gemeinsame Objekte, die für hochbelastete Szenarien nicht ideal sein können.

Was ist ein ValueTask?

ValueTask ist eine spezielle Struct-Wrapper, die entweder ein bereits fertiges Ergebnis oder (falls die Operation wirklich asynchron ist) den Task selbst repräsentieren kann. Einfach gesagt ist es ein "Paket", das entweder direkt das Ergebnis oder eine Referenz auf einen Task enthält.

Es gibt zwei Hauptvarianten:

  • ValueTask — "sinnlos" (gibt keinen Wert zurück, wie Task)
  • ValueTask<TResult> — Wrapper für einen Wert (Analog zu Task<TResult>)

Vergleich: Task vs ValueTask

Typ Anzahl Allocations Kann synchron abgeschlossen sein Wird üblicherweise verwendet
Task
Eine (Heap) Ja/Nein Praktisch immer
ValueTask
Keine/eine Ja/Nein Optimierung
ValueTask<T>
Keine/eine Ja/Nein Optimierung

Wann man ValueTask verwenden sollte

Es gibt eine goldene Regel: Wenn du einen normalen asynchronen Methodenschreibstil hast, der das Ergebnis immer erst nach einer async-Operation zurückgibt, benutze Task. Das ist einfach, sicher und für alle verständlich.

Nutze ValueTask, wenn:

  • das Ergebnis synchron verfügbar sein kann (z. B. aus Cache, Pool, Speicher) und du Allocations sparen willst.
  • die asynchrone Operation nicht sehr häufig auftritt (ansonsten gehen die Vorteile in der Komplexität und den Kopierkosten der Struct verloren).

Achtung! Wenn du immer ein asynchrones Ergebnis zurückgibst — nimm ruhig Task. Wenn du dein API "super-optimiert" für häufige sofortige Ergebnisse machen willst — dann ValueTask.

2. Synchrones oder asynchrones Ergebnis

Schauen wir uns eine Funktion an, die einen Benutzer nach Name sucht. Wenn der Benutzer im Cache ist — geben wir ihn sofort zurück; wenn nicht — laden wir asynchron aus der "Datenbank":


// Wir modellieren unseren Benutzer
public class User
{
    public string Name { get; set; }
}

// Unser Cache (sehr einfach)
private readonly Dictionary<string, User> _localCache = new();

public async ValueTask<User> FindUserAsync(string name)
{
    // Prüfen des lokalen Caches
    if (_localCache.TryGetValue(name, out var user))
    {
        // Ergebnis ist sofortig — keine Task-Allocation!
        return user;
    }

    // Hier eine angeblich lange asynchrone Operation (z. B. aus der Datenbank)
    user = await LoadUserFromDbAsync(name);
    // In den Cache für die Zukunft legen
    _localCache[name] = user;
    return user;
}

private async Task<User> LoadUserFromDbAsync(string name)
{
    // Verzögerung simulieren
    await Task.Delay(500);
    return new User { Name = name };
}

Wichtig: Im Cache-Hit-Fall geben wir das normale Ergebnis zurück — es wird kein Task alloziert! Nur wenn das Ergebnis "erarbeitet" werden muss, erzeugen wir tatsächlich eine asynchrone Task.

Wie ValueTask innen aufgebaut ist

ValueTask kann intern entweder einen fertigen Wert oder eine Referenz auf einen Task enthalten:

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

Beim Verwenden von await kümmert sich der Compiler selbst: Wenn das Ergebnis sofort da ist, werden keine zusätzlichen Objekte erzeugt.

Wichtiges zu await und ValueTask

async-Methoden arbeiten gut mit await auf ValueTask:

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

Aber wenn du eine Instanz von ValueTask speicherst und später darauf wartest, denk daran: du darfst nicht denselben Wert mehr als einmal awaiten. Task kann mehrfach erwartet werden, ValueTask nicht.

Fehler: erneutes awaiten eines ValueTask

ValueTask<int> task = ComputeAsync();

// Das ist OK
int a = await task;

// Das ist ein Fehler! Ein zweites await auf derselben ValueTask-Instanz ist nicht erlaubt:
// int b = await task; // NICHT ERLAUBT!

3. Wir überarbeiten einen Teil unserer Konsolenanwendung

Angenommen, wir entwickeln jetzt einen Mini-Book-Reader: Teile der Texte sind bereits im Cache, der Rest wird asynchron nachgeladen. Implementieren wir eine optimierte Methode, die die erste Zeile eines Buches mit ValueTask zurückgibt:

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

    // Angenommen, asynchroner Download des Buches
    var downloadedBook = await DownloadBookTextAsync(title);
    _bookCache[title] = downloadedBook;
    return downloadedBook.Split('\n')[0];
}

private async Task<string> DownloadBookTextAsync(string title)
{
    // Verzögerung simulieren (z. B. Download aus dem Internet)
    await Task.Delay(1000);
    return $"Book: {title}\nThis is the first line.\nSecond line...";
}

So spart ValueTask die Erstellung eines Tasks, wenn das Buch bereits im Cache ist.

4. Nützliche Feinheiten

Marker: wann du kein ValueTask verwenden solltest

Wenn dein API immer asynchron ist (z. B. immer Netzwerkommunikation) — benutze Task, das ist einfacher und sicherer.

Wie man ValueTask in Task und umgekehrt wandelt

Manchmal passt ValueTask nicht in ein API, das ein Task erwartet. Verwende .AsTask():

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

Wenn du aus einem Task einen ValueTask machen willst — erstelle einfach einen ValueTask aus dem Task:

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

Vergleich: ValueTask vs Task

Eigenschaft
Task
ValueTask
Erneutes await Erlaubt Nicht erlaubt
Allocations bei schnellen Antworten Ja Nein (wenn Ergebnis sofort)
Interface-Kompatibilität Weit verbreitet Erfordert zusätzliche Wrapper
Anwendbarkeit Überall Nur zur Optimierung
Pooling Verwendet gemeinsamen Pool Nein, ist eine Struct
Einfachheit Einfach Komplexer

Einsatz von ValueTask im echten Leben

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

Im Vorstellungsgespräch anwendbar: Wenn man dich fragt, wie man die Performance eines asynchronen, cache-basierten APIs noch weiter verbessern kann — erwähne ValueTask.

Beispiele zur Kompatibilität mit LINQ und IAsyncEnumerable<T>

Wenn du ValueTask mit asynchronem LINQ (IAsyncEnumerable<T>) verwenden willst, wird das in .NET unterstützt (z. B. die Methode ToListAsync):

public async ValueTask<List<int>> GetPrimeNumbersAsync()
{
    // Simulieren, dass ein Teil der Zahlen bereits berechnet ist und ein Teil asynchron kommt
    // ... (Beispiel aus Platzgründen ausgelassen)
    return new List<int> { 2, 3, 5, 7, 11 };
}

5. Fazit und Implementierungsbesonderheiten

  1. Verwende ValueTask zur Optimierung, wenn ein großer Teil der Zeit dein Ergebnis sofort bereitsteht (z. B. aus Cache).
  2. Verwende ValueTask nicht "einfach so" — das macht den Code komplizierter und führt zu Fehlern.
  3. Await-ein ValueTask nicht mehrfach. Wenn nötig, konvertiere ihn in einen Task.
  4. Die gesamte Kompatibilitätslogik mit Task läuft über .AsTask().
  5. Denk dran: ValueTask ist ein struct und kann sich beim Kopieren anders verhalten.

6. Typische Fehler beim Arbeiten mit ValueTask

Fehler Nr.1: mehrfaches await auf derselben ValueTask-Instanz. ValueTask ist eine Struct und darf nicht zweimal await-et werden. Ein zweites await führt zu einer Exception oder zu falschem Verhalten.

Fehler Nr.2: Verwendung von ValueTask ohne Prüfung der Kompatibilität. Manche Drittanbieter-APIs erwarten ein Task, und direktes Weiterreichen eines ValueTask kann Probleme machen.

Fehler Nr.3: Einsatz von ValueTask ohne Notwendigkeit. Wenn das Ergebnis immer asynchron ist, macht ValueTask den Code nur komplizierter ohne wirklichen Nutzen.

Fehler Nr.4: Kopieren der ValueTask-Struct. Eine kopierte Struct kann beim await überraschend anders reagieren, arbeite möglichst mit dem Original.

2
Aufgabe
C# SELF, Level 62, Lektion 2
Gesperrt
Das Ergebnis sofort oder asynchron zurückgeben
Das Ergebnis sofort oder asynchron zurückgeben
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION