ValueTask

Task vs. ValueTask in C#

vg

In der modernen Anwendungsentwicklung ist Reaktionsfähigkeit der Schlüssel, und der Aufbau reaktionsfähiger Anwendungen hängt stark von der asynchronen Programmierung ab, insbesondere wenn es um E/A-gebundene Aufgaben geht. Ganz gleich, ob Sie Datenbankabfragen verarbeiten, auf Dateien zugreifen oder API-Aufrufe tätigen, die asynchrone Programmierung sorgt dafür, dass Ihre Anwendung schnell und benutzerfreundlich bleibt. Wenn Sie mit async/await in .NET gearbeitet haben, haben Sie wahrscheinlich Task verwendet, um asynchrone Operationen darzustellen. Aber wussten Sie, dass es noch eine andere Möglichkeit gibt? In diesem Blogbeitrag möchte ich Ihnen ValueTask vorstellen – eine leichtgewichtige Alternative zu Task, die für Szenarien entwickelt wurde, in denen Leistung und Ressourceneffizienz eine Rolle spielen. Obwohl beide Typen denselben Zweck erfüllen, sind sie für unterschiedliche Anwendungsfälle geeignet. Wenn Sie die Unterschiede zwischen Task und ValueTask verstehen, können Sie effizienteren und besser wartbaren Code schreiben.

Lassen Sie uns diese Typen aufschlüsseln, um ihre Unterschiede zu verstehen und zu verstehen, wann Sie sie verwenden sollten.

Was ist ein Task?

Ein Task stellt eine asynchrone Operation in .NET dar. Wenn Sie eine asynchrone Methode aufrufen, gibt diese in der Regel einen Task zurück, der den Vorgang im Hintergrund ausführt und schließlich ein Ergebnis liefert. Die Einfachheit von Task macht ihn zur ersten Wahl für die meisten asynchronen Methoden. Einige wichtige Dinge über Task:

  • Schwergewicht: Task ist eine Klasse, d. h. sie benötigt mehr Speicherplatz. Wenn sie erstellt wird, belegt sie Platz auf dem Heap.
  • Wiederverwendbar: Sobald ein Task abgeschlossen ist, kann er in verschiedenen Kontexten wiederverwendet werden, was ihn für verschiedene Szenarien sehr flexibel macht.
  • Fehlerbehandlung: Task unterstützt die Behandlung von Ausnahmen, die beim Abfangen von Fehlern während asynchroner Operationen helfen.

So ist beispielsweise das Lesen oder Schreiben einer Datei in der Regel ein langwieriger, E/A-gebundener Vorgang, der asynchron ausgeführt werden sollte. Da das Lesen aus einer Datei relativ langsam ist und wahrscheinlich nicht synchron abgeschlossen werden kann, ist Task für dieses Szenario gut geeignet.

public async Task<string> ReadFileAsync(string filePath)
{
using (var reader = new StreamReader(filePath))
{
return await reader.ReadToEndAsync();
}
}

In diesem Fall ist jeder Aufruf mit erheblicher Arbeit verbunden (Lesen von der Festplatte) und es ist unwahrscheinlich, dass er sofort abgeschlossen wird.

Diese Methode kann auch in verschiedenen Kontexten verwendet werden, ohne dass es Einschränkungen hinsichtlich des mehrfachen Wartens oder der Weitergabe in Ihrer Anwendung gibt, was den Fähigkeiten von Task entspricht.

Task ist auch mit der Fehlerbehandlung kompatibel, wiederverwendbar und kann die asynchrone Fortsetzung auf natürliche Weise verarbeiten.

Aus diesem Grund eignet sich Task gut für Datei-E/A-Operationen.

Was ist ValueTask?

ValueTask ist eine leichtgewichtige Alternative zu Task. Sie wurde in .NET eingeführt, um die Leistung in bestimmten Szenarien zu verbessern, in denen die Erstellung eines vollständigen Tasks unnötig ist. Es ist besonders nützlich, wenn Sie erwarten, dass das Ergebnis einer asynchronen Operation schnell oder in manchen Fällen synchron zur Verfügung steht.

Hauptmerkmale von ValueTask:

  • Effizienter Speicher: Im Gegensatz zu Task ist ValueTask eine Struktur. Das bedeutet, dass sie auf dem Stack gespeichert werden kann, was Heap-Zuweisungen reduziert und die Speichereffizienz verbessert.
  • Bedingte Verwendung: ValueTask sollte nur verwendet werden, wenn Sie wissen, dass eine asynchrone Methode synchron oder sehr schnell abgeschlossen werden kann. Sie ist ideal für Szenarien wie Caching-Operationen oder kleine Hintergrundaufgaben.
  • Einmalige Nutzung: Im Gegensatz zu Task ist ValueTask nicht wieder verwendbar. Einmal erwartet, sollten Sie es nicht noch einmal erwarten, da es nur für die einmalige Verwendung gedacht ist.
  • Nicht blockierend: ValueTask kann nicht blockiert werden, d. h. Sie können nicht synchron auf seine Fertigstellung warten (z. B. mit .Wait()). Verbrauchen Sie eine ValueTask immer mit await, um ein korrektes Verhalten sicherzustellen.

Nehmen wir zum Beispiel eine Methode, die Daten aus einem In-Memory-Cache abruft. Wenn die Daten im Speicher verfügbar sind, können sie sofort (synchron) zurückgegeben werden. Andernfalls werden die Daten asynchron aus einer Datenbank abgerufen. Hier kann ValueTask die unnötige Zuweisung eines Tasks einsparen, wenn das Ergebnis sofort verfügbar ist.

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

public async ValueTask<int> GetCachedValueAsync(string key)
{
if (_cache.TryGetValue(key, out int cachedValue))
{
return cachedValue; // Synchronous completion
}

// If not in cache, simulate async database call
int dbValue = await FetchFromDatabaseAsync(key);
_cache[key] = dbValue; // Cache the result
return dbValue;
}

private async Task<int> FetchFromDatabaseAsync(string key)
{
await Task.Delay(100); // Simulate database delay
return new Random().Next(1, 100); // Simulated data
}

Beachten Sie, dass GetCachedValueAsync synchron abgeschlossen werden kann, wenn der Wert im Cache gefunden wird. Durch die Verwendung von ValueTask wird in diesen Fällen eine unnötige Speicherzuweisung vermieden. Es wird Speicher gespart, da kein neues Task-Objekt erstellt wird, wenn der Wert aus dem Cache synchron zurückgegeben wird.

Hinweis: Beachten Sie, dass ValueTask nicht mehrfach erwartet werden sollte. Sobald das Ergebnis von GetCachedValueAsync abgerufen wurde, sollten Sie es nicht erneut abwarten.

ValueTask blockieren

Technisch gesehen können Sie eine ValueTask zwar blockieren oder wiederverwenden, aber das verstößt gegen die empfohlene Praxis der asynchronen Programmierung und untergräbt die Leistungsvorteile und Korrektheitsgarantien von ValueTask. Kurz gesagt, der Hauptnutzen wird verschwinden. Aus diesem Grund sollten Sie für ValueTask-Operationen immer await bevorzugen.

Wenn Sie jedoch aus irgendeinem Grund blockieren müssen oder mehr als einmal await benötigen, können Sie eine ValueTask erhalten, indem Sie sie mit den Methoden .AsTask() oder .Preserve() in eine Task umwandeln. Auf diese Weise wird eine Standard-Task-Instanz erzeugt, die sicher mehrfach gewartet werden kann.

// Correct Usage
var cachedValue = await GetCachedValueAsync("key");

// Convert to a Task:
var task = GetCachedValueAsync("key").AsTask();

// Or Preserve:
var preservedTask = GetCachedValueAsync("key").Preserve();

// Only if absolutely necessary, but still discouraged.
task.Wait();
preservedTask.Wait();

Wiederverwendung von ValueTask

Andererseits kann die Wiederverwendung oder falsche Erwartung einer ValueTask zu unerwartetem Verhalten oder Laufzeitfehlern führen, da ValueTask eine Struktur ist und nicht immer ein eindeutiges, einzelnes Ergebnis wie eine Task darstellt.

// Unsafe: Awaiting a ValueTask multiple times directly
ValueTask<int> valueTask = GetCachedValueAsync("key");

// Results are NOT guaranteed to have the same value.
int value1 = await valueTask;
int value2 = await valueTask; // Error: ValueTask can only be awaited once!

// Safe: Preserve by converting to Task
Task<int> preservedTask = valueTask.AsTask();

// Results always be the same.
int value3 = await preservedTask;
int value4 = await preservedTask; // Safe: Returns already calculated result.

Beachten Sie, dass die Wiederverwendung von ValueTask nicht per se den gleichen Wert garantiert. Wenn Sie eine ValueTask wiederverwenden und ihr ein synchroner Wert zugrunde liegt (z. B. ein Literalwert oder ein Ergebnis), kann die Wiederverwendung denselben Wert ergeben. Andernfalls liegt ihr eine zustandsabhängige asynchrone Operation zugrunde (z. B. ein E/A-Aufruf oder eine asynchrone Methode), und jede Verwendung könnte eine neue Berechnung auslösen, die möglicherweise unterschiedliche Ergebnisse liefert.

Wann sollte man welches verwenden?

In den meisten Fällen sollte Task Ihre Standardwahl für asynchrone Arbeit in .NET sein. Es ist einfach zu verwenden, wird umfassend unterstützt und eignet sich perfekt für die überwiegende Mehrheit der asynchronen Szenarien. Ziehen Sie ValueTask nur in speziellen, leistungsstarken Fällen in Betracht, in denen die Minimierung von Speicherzuweisungen und die Maximierung der Effizienz von entscheidender Bedeutung sind.

Hier ist eine schnelle Entscheidungshilfe:

Verwenden Sie Task, wenn:

  • Der Vorgang hat eine lange Ausführungszeit oder ist von Natur aus asynchron, z. B. Netzwerk- oder Datenträger-E/A.
  • Das Ergebnis der Aufgabe wird mehrmals erwartet.
  • Sie entwickeln eine öffentliche API, bei der Verbraucher möglicherweise Wiederverwendbarkeit benötigen.
  • Sie müssen nicht speziell für Allokationen optimieren.

Verwenden Sie die Option ValueTask, wenn:

  • Der Vorgang kann synchron abgeschlossen werden, z. B. ein zwischengespeichertes Ergebnis.
  • Die Methode wird in einer engen Schleife aufgerufen, in der Speicherbelegungen kostspielig sind.
  • Sie haben durch die Profilerstellung überprüft, dass Zuordnungen von Task-Objekten Leistungsprobleme verursachen.

Task und ValueTask haben also beide ihre Stärken, und die Wahl der richtigen Methode kann zur Verbesserung der Leistung beitragen.

Wichtige Dinge, die Sie beachten sollten

  • Wiederverwendung: ValueTask ist nicht wiederverwendbar. Bleiben Sie also bei Task, wenn Sie die Flexibilität benötigen, das Ergebnis wiederzuverwenden.
  • Fehlerbehandlung: Sowohl Task als auch ValueTask unterstützen Ausnahmen. Task wird jedoch häufiger verwendet und ist für Szenarien mit komplexer Fehlerbehandlung besser geeignet.
  • Kompatibilität: Viele .NET-Bibliotheken und -Frameworks wurden mit Blick auf Task entwickelt. Die Verwendung von ValueTask kann zu Kompatibilitätsproblemen bei der Integration in bestehende Codebasen oder APIs führen, die Task erwarten.

Fazit

In den meisten Fällen ist das Beibehalten von Task einfacher und mit vorhandenem Code kompatibel. Wenn Sie jedoch in einem Hochleistungsbereich arbeiten, in dem die Reduzierung der Speicherzuweisung von entscheidender Bedeutung ist, kann valueTask eine gute Option sein. Die Verwendung von Task für allgemeine asynchrone Methoden und die Reservierung von ValueTask für spezielle, optimierte Fälle ist ein solider Ansatz.

Weiterer Betrag: Das Problem mit Microservices

com

Newsletter Anmeldung

Bleiben Sie informiert! Wir informieren Sie über alle neuen Beiträge (max. 1 Mail pro Woche – versprochen)