Diese Übersetzung entstand dank des guten Kommentars 0x1000000 .

Mit .NET Framework 4 wurde der System.Threading.Tasks-Bereich und damit die Task-Klasse eingeführt. Dieser Typ und die daraus generierte Task <TResult> haben lange gewartet, bis sie von den Standards in .NET als Schlüsselaspekte des asynchronen Programmiermodells erkannt werden, das in C # 5 mit seinen async / await-Anweisungen eingeführt wurde. In diesem Artikel werde ich über neue Arten von ValueTask / ValueTask <TResult> sprechen, mit denen die Leistung asynchroner Methoden in Fällen verbessert werden soll, in denen der Overhead der Speicherzuweisung berücksichtigt werden sollte.
Aufgabe
Die Aufgabe spielt in verschiedenen Rollen, aber die Hauptaufgabe ist das „Versprechen“ (Versprechen), ein Objekt, das den möglichen Abschluss einer Operation darstellt. Sie initiieren eine Operation und erhalten ein Task-Objekt dafür, das ausgeführt wird, wenn die Operation abgeschlossen ist. Dies kann im synchronen Modus als Teil der Initialisierung der Operation (z. B. Empfangen von Daten, die sich bereits im Puffer befinden) im asynchronen Modus mit Ausführung zum Zeitpunkt erfolgen Sie erhalten Task (Daten werden nicht aus dem Puffer empfangen, sondern sehr schnell) oder im asynchronen Modus, aber nachdem Sie bereits Task haben (Daten von einer Remote-Ressource empfangen). Da der Vorgang asynchron beendet werden kann, blockieren Sie entweder den Ausführungsfluss und warten auf das Ergebnis (wodurch die Asynchronität des Aufrufs häufig bedeutungslos wird) oder erstellen eine Rückruffunktion, die nach Abschluss des Vorgangs aktiviert wird. In .Net 4 wird die Erstellung eines Rückrufs durch die ContinueWith-Methoden des Task-Objekts implementiert, die dieses Modell deutlich demonstrieren, indem sie eine Delegatfunktion akzeptieren, um es auszuführen, nachdem die Task ausgeführt wurde:
SomeOperationAsync().ContinueWith(task => { try { TResult result = task.Result; UseResult(result); } catch (Exception e) { HandleException(e); } });
In .NET Framework 4.5 und C # 5 können Task-Objekte einfach vom Operator await aufgerufen werden, wodurch das Ergebnis einer asynchronen Operation leicht abgerufen werden kann. Der generierte Code, der für die oben genannten Optionen optimiert ist, funktioniert in allen Fällen ordnungsgemäß, wenn die Operation im synchronen Modus, schnell asynchron oder ausgeführt wird asynchron mit Rückruf:
TResult result = await SomeOperationAsync(); UseResult(result);
Aufgabe ist eine sehr flexible Klasse und hat eine Reihe von Vorteilen. Sie können beispielsweise mehrmals auf eine beliebige Anzahl von Verbrauchern gleichzeitig warten. Sie können es in eine Sammlung (ein Wörterbuch) einfügen, damit es in Zukunft wiederholt abgewartet werden kann, um es als Cache für die Ergebnisse asynchroner Aufrufe zu verwenden. Sie können die Ausführung blockieren, während Sie auf den Abschluss der Aufgabe warten, falls erforderlich. Außerdem können Sie verschiedene Vorgänge auf Aufgabenobjekte schreiben und anwenden (manchmal auch als „Kombinatoren“ bezeichnet), z. B. „Wenn vorhanden“, um asynchron auf den ersten Abschluss mehrerer Aufgaben zu warten.
Diese Flexibilität wird jedoch im häufigsten Fall überflüssig: Rufen Sie einfach die asynchrone Operation auf und warten Sie, bis die Aufgabe abgeschlossen ist:
TResult result = await SomeOperationAsync(); UseResult(result);
Hier müssen wir nicht mehrmals auf die Ausführung warten. Wir müssen nicht sicherstellen, dass die Erwartungen wettbewerbsfähig sind. Wir müssen keine synchrone Verriegelung durchführen. Wir werden keine Kombinatoren schreiben. Wir warten nur darauf, dass das Versprechen eines asynchronen Vorgangs abgeschlossen wird. Am Ende schreiben wir auf diese Weise synchronen Code (z. B. TResult result = SomeOperation ();) und dieser wird normalerweise in async / await übersetzt.
Darüber hinaus weist Task eine potenzielle Schwäche auf, insbesondere wenn eine große Anzahl von Instanzen erstellt wird und ein hoher Durchsatz und eine hohe Leistung wichtige Anforderungen sind - Task ist eine Klasse. Dies bedeutet, dass jede Operation, die eine Aufgabe benötigt, gezwungen ist, ein Objekt zu erstellen und zu platzieren. Je mehr Objekte erstellt werden, desto mehr Arbeit ist für den Garbage Collector (GC) erforderlich, und diese Arbeit verbraucht Ressourcen, die wir für etwas mehr ausgeben könnten nützlich.
Die Laufzeit- und Systembibliotheken tragen in vielen Situationen zur Minderung dieses Problems bei. Wenn wir zum Beispiel eine Methode wie diese schreiben:
public async Task WriteAsync(byte value) { if (_bufferedCount == _buffer.Length) { await FlushAsync(); } _buffer[_bufferedCount++] = value; }
In der Regel ist genügend freier Speicherplatz im Puffer vorhanden, und die Operation wird synchron ausgeführt. In diesem Fall müssen Sie nichts mit der Aufgabe tun, die zurückgegeben werden soll. Da es keinen Rückgabewert gibt, wird Task als Äquivalent einer synchronen Methode verwendet, die einen leeren Wert zurückgibt (void). Daher kann die Umgebung einfach eine nicht generische Aufgabe zwischenspeichern und sie als Ergebnis der Ausführung für jede asynchrone Methode, die synchron beendet wird, immer wieder verwenden (dieser zwischengespeicherte Singleton kann über Task.CompletedTask abgerufen werden). Oder Sie schreiben zum Beispiel:
public async Task<bool> MoveNextAsync() { if (_bufferedCount == 0) { await FillBuffer(); } return _bufferedCount > 0; }
Erwarten Sie im Allgemeinen, dass sich die Daten bereits im Puffer befinden. Die Methode überprüft daher einfach den Wert von _bufferedCount, stellt fest, dass er größer als 0 ist, und gibt true zurück. und nur wenn sich noch keine Daten im Puffer befinden, müssen Sie eine asynchrone Operation ausführen. Und da es nur zwei mögliche Ergebnisse vom Typ Boolean gibt (true und false), gibt es nur zwei mögliche Task-Objekte, die zur Darstellung dieser Ergebnisse benötigt werden. Die Umgebung kann diese Objekte zwischenspeichern und mit dem entsprechenden Wert zurückgeben, ohne Speicher zuzuweisen. Nur im Falle eines asynchronen Abschlusses muss die Methode eine neue Aufgabe erstellen, da diese zurückgegeben werden muss, bevor das Ergebnis der Operation bekannt ist.
Die Umgebung bietet Caching für einige andere Typen, es ist jedoch unrealistisch, alle möglichen Typen zwischenzuspeichern. Zum Beispiel die folgende Methode:
public async Task<int> ReadNextByteAsync() { if (_bufferedCount == 0) { await FillBuffer(); } if (_bufferedCount == 0) { return -1; } _bufferedCount--; return _buffer[_position++]; }
wird auch oft synchron ausgeführt. Im Gegensatz zu einer Variante mit einem Ergebnis vom Typ Boolean gibt diese Methode Int32 zurück, das ungefähr 4 Milliarden Werte hat, und das Zwischenspeichern aller Varianten von Task <int> erfordert Hunderte von Gigabyte Speicher. Die Umgebung bietet einen kleinen Cache für die Aufgabe <int>, aber einen sehr begrenzten Satz von Werten. Wenn diese Methode beispielsweise synchron (die Daten befinden sich bereits im Puffer) mit dem Rückgabewert 4 beendet wird, handelt es sich um eine zwischengespeicherte Aufgabe. Wenn der Wert 42 zurückgegeben wird, müssen Sie einen neuen erstellen Task <int>, ähnlich wie beim Aufrufen von Task.FromResult (42).
Viele Bibliotheksmethoden versuchen dies zu glätten, indem sie ihren eigenen Cache bereitstellen. Beispielsweise endet eine Überlastung in .NET Framework 4.5 der MemoryStream.ReadAsync-Methode immer synchron, wenn Daten aus dem Speicher gelesen werden. ReadAsync gibt eine Task <int> zurück, wobei ein Int32-Ergebnis angibt, wie viele Bytes gelesen wurden. Diese Methode wird häufig in einer Schleife verwendet, häufig mit der gleichen erforderlichen Anzahl von Bytes für jeden Aufruf, und häufig wird dieser Bedarf vollständig erfüllt. Bei wiederholten Aufrufen von ReadAsync ist zu erwarten, dass die Task <int> synchron mit demselben Wert wie im vorherigen Aufruf zurückgegeben wird. Daher erstellt ein MemoryStream einen Cache für ein Objekt, das beim letzten erfolgreichen Aufruf zurückgegeben wurde. Wenn das Ergebnis beim nächsten Aufruf wiederholt wird, wird das zwischengespeicherte Objekt zurückgegeben. Wenn nicht, erstellen Sie ein neues Objekt mit Task.FromResult, speichern Sie es im Cache und geben Sie es zurück.
Es gibt jedoch viele andere Fälle, in denen die Operation synchron ausgeführt wird, das Task <TResult> -Objekt jedoch erstellt werden muss.
ValueTask <TResult> und synchrone Ausführung
All dies erforderte die Implementierung eines neuen Typs in .NET Core 2.0, der in früheren Versionen von .NET im NuGet System.Threading.Tasks.Extensions: ValueTask <TResult> -Paket verfügbar war.
ValueTask <TResult> wurde in .NET Core 2.0 als Struktur erstellt, die sowohl TResult als auch Task <TResult> umschließen kann. Dies bedeutet, dass es von der asynchronen Methode zurückgegeben werden kann. Wenn diese Methode synchron und erfolgreich ausgeführt wird, müssen Sie kein Objekt auf dem Heap platzieren: Sie können diese ValueTask <TResult> -Struktur einfach mit dem Wert TResult initialisieren und zurückgeben. Nur bei asynchroner Ausführung wird das Task <TResult> -Objekt platziert und von ValueTask <TResult> umbrochen (um die Größe der Struktur zu minimieren und den Fall einer erfolgreichen Ausführung zu optimieren, platziert die asynchrone Methode, die mit einer nicht unterstützten Ausnahme endet, auch das Task <TResult> ValueTask <TResult> schließt auch nur Task <TResult> ein und enthält kein zusätzliches Feld zum Speichern von Exception.
Auf dieser Grundlage sollte eine Methode wie MemoryStream.ReadAsync, die jedoch eine ValueTask <int> zurückgibt, sich nicht mit dem Caching befassen, sondern kann wie folgt geschrieben werden:
public override ValueTask<int> ReadAsync(byte[] buffer, int offset, int count) { try { int bytesRead = Read(buffer, offset, count); return new ValueTask<int>(bytesRead); } catch (Exception e) { return new ValueTask<int>(Task.FromException<int>(e)); } }
ValueTask <TResult> und asynchrone Ausführung
Die Möglichkeit, eine asynchrone Methode zu schreiben, die synchron abgeschlossen werden kann, ohne dass eine zusätzliche Platzierung für das Ergebnis erforderlich ist, ist ein großer Gewinn. Aus diesem Grund wurde ValueTask <TResult> in .NET Core 2.0 hinzugefügt, und neue Methoden, die wahrscheinlich in Anwendungen verwendet werden, die Leistung erfordern, werden jetzt mit der Rückgabe von ValueTask <TResult> anstelle von Task <TResult> angekündigt. Wenn wir beispielsweise .NET Core 2.1 eine neue ReadAsync-Überladung der Stream-Klasse hinzugefügt haben, um Speicher anstelle von Byte [] übergeben zu können, geben wir den ValueTask <int> -Typ zurück. In dieser Form können Stream-Objekte (bei denen die ReadAsync-Methode sehr häufig synchron ausgeführt wird, wie im vorherigen Beispiel für den MemoryStream) mit viel weniger Speicherzuweisung verwendet werden.
Wenn wir jedoch mit Diensten mit sehr hoher Bandbreite arbeiten, möchten wir dennoch die Speicherzuweisung so weit wie möglich vermeiden, was bedeutet, dass die Speicherzuweisung auch entlang der asynchronen Ausführungsroute reduziert und eliminiert wird.
Im Wartemodell benötigen wir für jede Operation, die asynchron abgeschlossen wird, die Möglichkeit, ein Objekt zurückzugeben, das den möglichen Abschluss der Operation darstellt: Der Aufrufer muss den Rückruf umleiten, der am Ende der Operation initiiert wird, und dies erfordert ein eindeutiges Objekt im Heap, das als Übertragungskanal für dienen kann diese besondere Operation. Dies bedeutet gleichzeitig nichts darüber, ob dieses Objekt nach Abschluss des Vorgangs wiederverwendet wird. Wenn dieses Objekt wiederverwendet werden kann, kann die API einen Cache für eines oder mehrere dieser Objekte organisieren und für sequentielle Operationen verwenden, in dem Sinne, dass nicht dasselbe Objekt für mehrere asynchrone Zwischenoperationen verwendet wird, sondern für nicht wettbewerbsorientierten Zugriff.
In .NET Core 2.1 wurde die ValueTask <TResult> -Klasse erweitert, um ein ähnliches Pooling und eine ähnliche Wiederverwendung zu unterstützen. Anstatt nur TResult oder Task <TResult> zu verpacken, kann eine überarbeitete Klasse eine neue IValueTaskSource <TResult> -Schnittstelle umschließen. Diese Schnittstelle bietet die grundlegenden Funktionen, die erforderlich sind, um eine asynchrone Operation mit einem ValueTask <TResult> -Objekt auf dieselbe Weise wie Task <TResult> zu begleiten:
public interface IValueTaskSource<out TResult> { ValueTaskSourceStatus GetStatus(short token); void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags); TResult GetResult(short token); }
Die GetStatus-Methode wird verwendet, um Eigenschaften wie ValueTask <TResult> .IsCompleted zu implementieren, die Informationen zurückgeben, ob eine asynchrone Operation ausgeführt oder abgeschlossen wird und wie sie abgeschlossen wird (erfolgreich oder nicht). Die OnCompleted-Methode wird vom wartenden Objekt verwendet, um einen Rückruf anzuhängen und die Ausführung ab dem Wartepunkt fortzusetzen, wenn der Vorgang abgeschlossen ist. Die GetResult-Methode wird benötigt, um das Ergebnis der Operation abzurufen. Nach dem Ende der Operation kann der Aufrufer das TResult-Objekt abrufen oder eine ausgelöste Ausnahme übergeben.
Die meisten Entwickler benötigen diese Schnittstelle nicht: Methoden geben einfach ein ValueTask <TResult> -Objekt zurück, das als Wrapper für ein Objekt erstellt werden kann, das diese Schnittstelle implementiert, und die aufrufende Methode bleibt im Dunkeln. Diese Schnittstelle ist für Entwickler gedacht, die bei Verwendung einer leistungskritischen API die Speicherzuweisung vermeiden müssen.
In .NET Core 2.1 gibt es mehrere Beispiele für eine solche API. Die bekanntesten Methoden sind Socket.ReceiveAsync und Socket.SendAsync, wobei beispielsweise in 2.1 neue Überladungen hinzugefügt wurden
public ValueTask<int> ReceiveAsync(Memory<byte> buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default);
Diese Überladung gibt eine ValueTask <int> zurück. Wenn der Vorgang synchron abgeschlossen wird, kann einfach eine ValueTask <int> mit dem entsprechenden Wert zurückgegeben werden:
int result = …; return new ValueTask<int>(result);
Bei asynchroner Beendigung kann ein Objekt aus dem Pool verwendet werden, das die Schnittstelle implementiert:
IValueTaskSource<int> vts = …; return new ValueTask<int>(vts);
Die Socket-Implementierung unterstützt ein solches Objekt im Pool für den Empfang und eines für die Übertragung, da nicht mehr als ein Objekt für jede Richtung gleichzeitig auf die Ausführung warten kann. Diese Überlastungen weisen selbst bei einer asynchronen Operation keinen Speicher zu. Dieses Verhalten wird in der NetworkStream-Klasse weiter deutlich.
In .NET Core 2.1 bietet Stream beispielsweise Folgendes:
public virtual ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken);
welches in NetworkStream neu definiert wird. Die NetworkStream.ReadAsync-Methode verwendet einfach die Socket.ReceiveAsync-Methode, sodass die Gewinne in Socket an NetworkStream gesendet werden und NetworkStream.ReadAsync auch keinen Speicher zuweist.
Unshared ValueTask
Wenn ValueTask <TResult> in .NET Core 2.0 angezeigt wurde, wurde nur der synchrone Ausführungsfall darin optimiert, um die Platzierung des Task <TResult> -Objekts auszuschließen, wenn der TResult-Wert bereits bereit ist. Dies bedeutete, dass die generische ValueTask-Klasse nicht benötigt wurde: Für den Fall der synchronen Ausführung konnte die Singleton-Task.CompletedTask einfach von der Methode zurückgegeben werden, und dies wurde von der Umgebung implizit in den asynchronen Methoden durchgeführt, die Task zurückgeben.
Mit dem Abrufen asynchroner Operationen ohne Zuweisung von Speicher ist die Verwendung der nicht gemeinsam genutzten ValueTask jedoch wieder relevant geworden. In .NET Core 2.1 haben wir die generischen Werte ValueTask und IValueTaskSource eingeführt. Sie bieten direkte Entsprechungen für generische Versionen für ähnliche Zwecke mit nur einem leeren Rückgabewert.
Implementieren Sie IValueTaskSource / IValueTaskSource <T>
Die meisten Entwickler sollten diese Schnittstellen nicht implementieren. Darüber hinaus ist es nicht so einfach. Wenn Sie sich dazu entscheiden, können mehrere Implementierungen in .NET Core 2.1 als Ausgangspunkt dienen, zum Beispiel:
- AwaitableSocketAsyncEventArgs
- AsyncOperation <Tresult>
- DefaultPipeReader
Um dies zu vereinfachen, möchten wir in .NET Core 3.0 die gesamte erforderliche Logik präsentieren, die im Typ ManualResetValueTaskSourceCore <TResult> enthalten ist. Diese Struktur kann in ein anderes Objekt eingebettet werden, das IValueTaskSource <TResult> und / oder IValueTaskSource implementiert, damit sie delegiert werden kann Diese Struktur macht den Großteil der Funktionalität aus. Weitere Informationen hierzu finden Sie unter https://github.com/dotnet/corefx/issues/32664 im dotnet / corefx-Repository.
ValueTasks-Anwendungsmuster
Auf den ersten Blick ist der Umfang von ValueTask und ValueTask <TResult> viel eingeschränkter als Task und Task <TResult>. Dies ist gut und wird sogar erwartet, da die Hauptverwendung darin besteht, einfach den Operator await zu verwenden.
Da sie jedoch wiederverwendete Objekte umschließen können, gibt es im Vergleich zu Task und Task <TResult> erhebliche Einschränkungen bei ihrer Verwendung, wenn Sie von der üblichen Art des einfachen Wartens abweichen. Im Allgemeinen sollten die folgenden Vorgänge niemals mit ValueTask / ValueTask <TResult> ausgeführt werden:
- Wiederholtes Warten ValueTask / ValueTask <TResult> Das Ergebnisobjekt wurde möglicherweise bereits entsorgt und in einer anderen Operation verwendet. Im Gegensatz dazu wechselt Task / Task <Tresult> niemals von einem abgeschlossenen in einen unvollständigen Zustand, sodass Sie ihn so oft wie nötig erneut erwarten und jedes Mal das gleiche Ergebnis erzielen können.
- Paralleles Warten ValueTask / ValueTask <TResult> Das Ergebnisobjekt erwartet die Verarbeitung mit jeweils nur einem Rückruf von einem Verbraucher. Der Versuch, gleichzeitig aus verschiedenen Flows zu warten, kann leicht zu Rennen und subtilen Programmfehlern führen. Darüber hinaus handelt es sich auch um einen spezifischeren Fall der vorherigen ungültigen "erneuten Warte" -Operation. Im Vergleich dazu bietet Task / Task <Tresult> eine beliebige Anzahl paralleler Wartezeiten.
- Verwenden von .GetAwaiter (). GetResult (), wenn der Vorgang noch nicht abgeschlossen ist. Die Implementierung von IValueTaskSource / IValueTaskSource <TResult> benötigt keine Sperrunterstützung, bis der Vorgang abgeschlossen ist, und wird dies höchstwahrscheinlich nicht tun. Daher wird ein solcher Vorgang definitiv zu Rennen führen und wahrscheinlich wird nicht wie erwartet ausgeführt. Task / Task <TResult> blockiert den aufrufenden Thread, bis die Task abgeschlossen ist.
Wenn Sie eine ValueTask oder ValueTask <TResult> erhalten haben, aber eine dieser drei Operationen ausführen müssen, können Sie .AsTask () verwenden, Task / Task <TResult> abrufen und dann mit dem empfangenen Objekt arbeiten. Danach können Sie diese ValueTask / ValueTask <TResult> nicht mehr verwenden.
Kurz gesagt lautet die Regel: Wenn Sie ValueTask / ValueTask <TResult> verwenden, müssen Sie entweder direkt darauf warten (möglicherweise mit .ConfigureAwait (false)) oder AsTask () aufrufen und nicht mehr verwenden:
// , ValueTask<int> public ValueTask<int\> SomeValueTaskReturningMethodAsync(); ... // GOOD int result = await SomeValueTaskReturningMethodAsync(); // GOOD int result = await SomeValueTaskReturningMethodAsync().ConfigureAwait(false); // GOOD Task<int> t = SomeValueTaskReturningMethodAsync().AsTask(); // WARNING ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); // , // // BAD: await ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = await vt; int result2 = await vt; // BAD: await ( ) ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); Task.Run(async () => await vt); Task.Run(async () => await vt); // BAD: GetAwaiter().GetResult(), ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = vt.GetAwaiter().GetResult();
Ich hoffe, es gibt ein fortgeschritteneres Muster, das Programmierer nur nach sorgfältiger Messung und Erzielung bedeutender Vorteile anwenden können. Die ValueTask / ValueTask <TResult> -Klassen verfügen über mehrere Eigenschaften, die den aktuellen Status der Operation melden. Beispielsweise gibt die IsCompleted-Eigenschaft true zurück, wenn die Operation abgeschlossen wurde (dh sie wird nicht mehr erfolgreich ausgeführt oder erfolgreich abgeschlossen oder nicht erfolgreich abgeschlossen), und die IsCompletedSuccessfully-Eigenschaft gibt nur true zurück Wenn es erfolgreich abgeschlossen wurde (während des Wartens und Empfangens des Ergebnisses wurde keine Ausnahme ausgelöst). Für die anspruchsvollsten Ausführungsthreads, bei denen der Entwickler die im asynchronen Modus anfallenden Kosten vermeiden möchte, können diese Eigenschaften vor einer Operation überprüft werden, die das ValueTask / ValueTask <TResult> -Objekt tatsächlich zerstört, z. B. .AsTask (). Bei der Implementierung von SocketsHttpHandler in .NET Core 2.1 liest der Code beispielsweise aus der Verbindung und empfängt eine ValueTask <int>. Wenn dieser Vorgang synchron ausgeführt wird, müssen wir uns keine Gedanken über eine vorzeitige Beendigung des Vorgangs machen. Wenn es jedoch asynchron ausgeführt wird, müssen wir die Interrupt-Verarbeitung anschließen, damit die Interrupt-Anforderung die Verbindung unterbricht. Da dies ein sehr stressiger Code ist, kann die Profilerstellung wie folgt strukturiert werden, wenn für die Profilerstellung die folgenden kleinen Änderungen erforderlich sind:
int bytesRead; { ValueTask<int> readTask = _connection.ReadAsync(buffer); if (readTask.IsCompletedSuccessfully) { bytesRead = readTask.Result; } else { using (_connection.RegisterCancellation()) { bytesRead = await readTask; } } }
Sollte jede neue asynchrone API-Methode eine ValueTask / ValueTask <TResult> zurückgeben?
Um kurz zu antworten: Nein, standardmäßig lohnt es sich immer noch, Task / Task <Tresult> zu wählen.
Wie oben hervorgehoben, sind Task und Task <Tresult> einfacher korrekt zu verwenden als ValueTask und ValueTask <TResult>. Solange die Leistungsanforderungen die praktischen Anforderungen nicht überwiegen, werden Task und Task <TResult> bevorzugt. Darüber hinaus sind mit der Rückgabe einer ValueTask <TResult> anstelle einer Task <TResult> geringe Kosten verbunden, dh Mikro-Benchmarks zeigen, dass das Warten auf Task <TResult> schneller ist als das Warten auf ValueTask <TResult>. Wenn Sie beispielsweise das Task-Caching verwenden, gibt Ihre Methode Task oder Task zurück. Aus Gründen der Leistung lohnt es sich, bei Task oder Task zu bleiben. ValueTask / ValueTask <TResult> -Objekte belegen mehrere Wörter im Speicher. Wenn sie erwartet werden und ihre Felder in der Zustandsmaschine reserviert sind, die die asynchrone Methode aufruft, belegen sie mehr Speicher darin.
- ValueTask/ValueTask<TResult> : ) , await, ) , ) , . , / .
ValueTask ValueTask<TResult>?
.NET , Task/Task<TResult>, , ValueTask/ValueTask<TResult>, , . – IAsyncEnumerator<T>, .NET Core 3.0. IEnumerator<T> MoveNext, bool, IAsyncEnumerator<T> MoveNextAsync. , , Task, . , , , ( ), await foreach, ValueTask. , . C# , , , .