ValueTask - warum, warum und wie?

Vorwort zur Übersetzung


Im Gegensatz zu wissenschaftlichen Artikeln ist es schwierig, Artikel dieser Art "nahe am Text" zu übersetzen, und es muss eine ziemlich starke Anpassung vorgenommen werden. Aus diesem Grund entschuldige ich mich für einige Freiheiten meinerseits beim Umgang mit dem Text des Originalartikels. Ich leite nur ein Ziel: die Übersetzung verständlich zu machen, auch wenn sie stellenweise stark vom Originalartikel abweicht. Ich wäre dankbar für konstruktive Kritik und Korrekturen / Ergänzungen der Übersetzung.


Einführung


Der System.Threading.Tasks Namespace und die Task Klasse wurden erstmals in .NET Framework 4 eingeführt. Seitdem sind dieser Typ und seine abgeleitete Klasse Task<TResult> fest in der Programmierpraxis in .NET Task<TResult> und zu Schlüsselaspekten des asynchronen Modells geworden. implementiert in C # 5 mit seiner async/await . In diesem Artikel werde ich auf die neuen Typen von ValueTask/ValueTask<TResult> , die eingeführt wurden, um die Leistung von asynchronem Code zu verbessern, wenn der Speicheraufwand eine Schlüsselrolle spielt.



Aufgabe


Task dient mehreren Zwecken, aber der Hauptzweck ist "Versprechen" - ein Objekt, das die Fähigkeit darstellt, auf den Abschluss einer Operation zu warten. Sie starten den Vorgang und erhalten Task . Diese Task wird abgeschlossen, wenn der Vorgang selbst abgeschlossen ist. In diesem Fall gibt es drei Möglichkeiten:


  1. Der Vorgang wird synchron im Initiator-Thread abgeschlossen. Zum Beispiel beim Zugriff auf Daten, die sich bereits im Puffer befinden .
  2. Die Operation wird asynchron ausgeführt, kann jedoch abgeschlossen werden, wenn der Initiator die Task empfängt. Zum Beispiel, wenn Sie schnell auf Daten zugreifen, die noch nicht gepuffert wurden
  3. Die Operation wird asynchron ausgeführt und endet, nachdem der Initiator die Task empfangen hat Task Ein Beispiel ist der Empfang von Daten über ein Netzwerk .

Um das Ergebnis eines asynchronen Aufrufs zu erhalten, kann der Client entweder den aufrufenden Thread blockieren, während er auf den Abschluss wartet, was häufig der Idee der Asynchronität widerspricht, oder eine Rückrufmethode bereitstellen, die nach Abschluss des asynchronen Vorgangs ausgeführt wird. Das Rückrufmodell in .NET 4 wurde explizit unter Verwendung der ContinueWith Methode eines Objekts der Task Klasse dargestellt, das einen Delegaten erhielt, der nach Abschluss der asynchronen Operation aufgerufen wurde.


 SomeOperationAsync().ContinueWith(task => { try { TResult result = task.Result; UseResult(result); } catch (Exception e) { HandleException(e); } }); 

Mit .NET Frmaework 4.5 und C # 5 wurde das Abrufen des Ergebnisses einer asynchronen Operation vereinfacht, indem die Schlüsselwörter async/await und der dahinter stehende Mechanismus eingeführt wurden. Dieser Mechanismus, der generierte Code, kann alle oben genannten Fälle optimieren und die Fertigstellung trotz des Pfades, auf dem sie erreicht wurde, korrekt handhaben.


 TResult result = await SomeOperationAsync(); UseResult(result); 

Die Task Klasse ist sehr flexibel und bietet mehrere Vorteile. Beispielsweise können Sie ein Objekt dieser Klasse mehrmals "erwarten". Sie können das Ergebnis von einer beliebigen Anzahl von Verbrauchern wettbewerbsfähig erwarten. Instanzen einer Klasse können für eine beliebige Anzahl nachfolgender Aufrufe in einem Wörterbuch gespeichert werden, mit dem Ziel, in Zukunft zu "warten". In den beschriebenen Szenarien können Sie Task Objekte als eine Art Cache mit Ergebnissen betrachten, die asynchron erhalten werden. Darüber hinaus bietet Task die Möglichkeit, den wartenden Thread zu blockieren, bis der Vorgang abgeschlossen ist, wenn das Skript dies erfordert. Es gibt auch die sogenannten. Kombinatoren für verschiedene Strategien zum Warten auf den Abschluss von Aufgabensätzen, z. B. "Task.WhenAny" - asynchrones Warten auf den Abschluss der ersten von vielen Aufgaben.


Der häufigste Anwendungsfall besteht jedoch darin, einfach eine asynchrone Operation zu starten und dann auf das Ergebnis ihrer Ausführung zu warten. Solch ein einfacher Fall, der durchaus üblich ist, erfordert nicht die oben genannte Flexibilität:


 TResult result = await SomeOperationAsync(); UseResult(result); 

Dies ist sehr ähnlich zu dem, wie wir synchronen Code schreiben (z. B. TResult result = SomeOperation(); ). Diese Option wird natürlich in async/await .


Darüber hinaus weist der Task trotz aller Vorzüge einen potenziellen Fehler auf. Task ist eine Klasse. Dies bedeutet, dass jede Operation, die eine Instanz einer Task erstellt, ein Objekt auf dem Heap zuweist. Je mehr Objekte wir erstellen, desto mehr Arbeit wird vom GC benötigt und desto mehr Ressourcen werden für die Arbeit des Garbage Collector aufgewendet, Ressourcen, die für andere Zwecke verwendet werden könnten. Dies wird zu einem klaren Problem für den Code, in dem einerseits häufig Taskinstanzen erstellt werden und andererseits die Anforderungen an Durchsatz und Leistung erhöht werden.


Die Laufzeit- und Hauptbibliotheken können diesen Effekt in vielen Situationen abschwächen. Wenn Sie beispielsweise eine Methode wie die folgende schreiben:


 public async Task WriteAsync(byte value) { if (_bufferedCount == _buffer.Length) { await FlushAsync(); } _buffer[_bufferedCount++] = value; } 

und meistens ist genügend Speicherplatz im Puffer vorhanden, die Operation wird synchron beendet. Wenn ja, dann hat die zurückgegebene Aufgabe nichts Besonderes, es gibt keinen Rückgabewert und der Vorgang ist bereits abgeschlossen. Mit anderen Worten, wir haben es mit Task tun, dem Äquivalent einer synchronen void Operation. In solchen Situationen Task.ComletedTask die Laufzeit das Task Objekt einfach zwischen und verwendet es jedes Mal als Ergebnis für jede async Task - eine Methode, die synchron beendet wird ( Task.ComletedTask ). Ein weiteres Beispiel: Nehmen wir an, Sie schreiben:


 public async Task<bool> MoveNextAsync() { if (_bufferedCount == 0) { await FillBuffer(); } return _bufferedCount > 0; } 

Nehmen wir auf die gleiche Weise an, dass sich in den meisten Fällen einige Daten im Puffer befinden. Die Methode überprüft _bufferedCount , _bufferedCount , dass die Variable größer als Null ist, und gibt true . Nur wenn zum Zeitpunkt der Überprüfung die Daten nicht gepuffert waren, ist eine asynchrone Operation erforderlich. Wie dem auch sei, es gibt nur zwei mögliche logische Ergebnisse ( true und false ) und nur zwei mögliche Rückgabezustände über Task<bool> . Basierend auf der synchronen Fertigstellung oder asynchron, jedoch vor dem Beenden der Methode, speichert die Laufzeit zwei Instanzen von Task<bool> (eine für true und eine für false ) zwischen und gibt die gewünschte zurück, wobei zusätzliche Zuordnungen vermieden werden. Die einzige Option, wenn Sie ein neues Task<bool> -Objekt erstellen müssen, ist ein Fall von asynchroner Ausführung, der nach der "Rückkehr" endet. In diesem Fall muss die Methode ein neues Task<bool> -Objekt erstellen, weil Zum Zeitpunkt des Beendens des Verfahrens ist das Ergebnis des Abschlusses der Operation noch nicht bekannt. Das zurückgegebene Objekt muss eindeutig sein, weil Es wird letztendlich das Ergebnis der asynchronen Operation speichern.


Es gibt andere Beispiele für ähnliches Caching aus der Laufzeit. Eine solche Strategie ist jedoch nicht überall anwendbar. Zum Beispiel die Methode:


 public async Task<int> ReadNextByteAsync() { if (_bufferedCount == 0) { await FillBuffer(); } if (_bufferedCount == 0) { return -1; } _bufferedCount--; return _buffer[_position++]; } 

endet auch oft synchron. Im Gegensatz zum vorherigen Beispiel gibt diese Methode jedoch ein ganzzahliges Ergebnis mit ungefähr vier Milliarden möglichen Werten zurück. Zum Zwischenspeichern von Task<int> wären in dieser Situation Hunderte von Gigabyte Speicher erforderlich. Die Umgebung unterstützt hier auch einen kleinen Cache für Task<int> für mehrere kleine Werte. Wenn der Vorgang beispielsweise synchron abgeschlossen wird (Daten sind im Puffer vorhanden), mit einem Ergebnis von 4, wird der Cache verwendet. Wenn das Ergebnis jedoch synchron ist, ist der Abschluss 42, und ein neues Task<int> -Objekt wird erstellt, ähnlich wie beim Aufrufen von Task.FromResult(42) .


Viele Bibliotheksimplementierungen versuchen, diese Situationen durch die Unterstützung ihrer eigenen Caches zu entschärfen. Ein Beispiel ist die Überlastung von MemoryStream.ReadAsync . Dieser in .NET Framework 4.5 eingeführte Vorgang endet immer synchron, weil es ist nur eine Lesung aus dem Gedächtnis. ReadAsync gibt eine Task<int> wobei das ganzzahlige Ergebnis die Anzahl der gelesenen Bytes darstellt. Sehr oft tritt im Code eine Situation auf, wenn ReadAsync in einer Schleife verwendet wird. Darüber hinaus, wenn die folgenden Symptome auftreten:


  • Die Anzahl der angeforderten Bytes ändert sich für die meisten Iterationen der Schleife nicht.
  • In den meisten Iterationen kann ReadAsync die angeforderte Anzahl von Bytes lesen.

Das heißt, bei wiederholten Aufrufen wird ReadAsync synchron ausgeführt und gibt ein Task<int> -Objekt mit demselben Ergebnis von Iteration zu Iteration zurück. Es ist logisch, dass MemoryStream die zuletzt erfolgreich abgeschlossene Aufgabe zwischenspeichert und bei allen nachfolgenden Aufrufen eine Instanz aus dem Cache zurückgibt, wenn das neue Ergebnis mit dem vorherigen übereinstimmt. Wenn das Ergebnis nicht übereinstimmt, wird Task.FromResult verwendet, um eine neue Instanz zu erstellen, die wiederum vor der Rückkehr zwischengespeichert wird.


Es gibt jedoch viele Fälle, in denen eine Operation gezwungen ist, neue Task<TResult> -Objekte zu erstellen, selbst wenn sie synchron abgeschlossen ist.


ValueTask <TResult> und synchroner Abschluss


All dies diente letztendlich als Motivation für die Einführung eines neuen ValueTask<TResult> in .NET Core 2.0. System.Threading.Tasks.Extensions wurde dieser Typ über das Nuget-Paket System.Threading.Tasks.Extensions in anderen .NET-Versionen verfügbar gemacht.


ValueTask<TResult> wurde in .NET Core 2.0 als Struktur eingeführt, die TResult oder Task<TResult> . Dies bedeutet, dass Objekte dieses Typs von der async Methode zurückgegeben werden können. Das erste Plus aus der Einführung dieses Typs ist sofort sichtbar: Wenn die Methode erfolgreich und synchron abgeschlossen wurde, muss auf dem Heap nichts erstellt werden, gerade genug, um eine Instanz von ValueTask<TResult> mit dem Ergebniswert zu erstellen. Nur wenn die Methode asynchron beendet wird, müssen wir eine Task<TResult> erstellen. In diesem Fall wird ValueTask<TResult> als Wrapper für Task<TResult> . Die Entscheidung, ValueTask<TResult> Lage zu ValueTask<TResult> , Task<TResult> zu aggregieren, wurde zur Optimierung getroffen: Im Erfolgsfall und im Task<TResult> erstellt die asynchrone Methode Task<TResult> . Aus Sicht der Speicheroptimierung ist es besser, das Objekt Task<TResult> zu aggregieren Task<TResult> als zusätzliche Felder in der ValueTask<TResult> für verschiedene Abschlussfälle ValueTask<TResult> (z. B. um eine Ausnahme zu speichern).


Vor diesem Hintergrund ist das Zwischenspeichern in Methoden wie dem oben genannten MemoryStream.ReadAsync nicht mehr erforderlich, sondern kann wie folgt implementiert 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 Beendigung


Die Fähigkeit, asynchrone Methoden zu schreiben, für die keine zusätzlichen Speicherzuweisungen für das Ergebnis erforderlich sind, mit synchroner Vervollständigung, ist wirklich ein großes Plus. Wie oben erwähnt, war dies das Hauptziel für die Einführung des neuen ValueTask<TResult> in .NET Core 2.0. Alle neuen Methoden, die voraussichtlich auf den "heißen Straßen" verwendet werden, verwenden jetzt ValueTask<TResult> anstelle von Task<TResult> als Rückgabetyp. Beispielsweise gibt eine neue Überladung der ReadAsync Methode für Stream in .NET Core 2.1 (bei der Memory<byte> anstelle von byte[] als Parameter verwendet wird) eine Instanz von ValueTask<int> . Dadurch konnte die Anzahl der Zuweisungen beim Arbeiten mit Streams erheblich reduziert werden (sehr oft wird die ReadAsync Methode synchron beendet, wie im Beispiel mit MemoryStream ).


Bei der Entwicklung von Diensten mit hoher Bandbreite, bei denen eine asynchrone Beendigung nicht ungewöhnlich ist, müssen wir jedoch unser Bestes tun, um zusätzliche Zuweisungen zu vermeiden.


Wie bereits erwähnt, muss im asynchronen async/await Modell jede Operation, die asynchron ausgeführt wird, ein eindeutiges Objekt zurückgeben, um auf den Abschluss zu warten. Einzigartig, weil Es dient als Kanal für die Durchführung von Rückrufen. Beachten Sie jedoch, dass diese Konstruktion nichts darüber aussagt, ob das zurückgegebene Warteobjekt nach Abschluss der asynchronen Operation wiederverwendet werden kann . Wenn ein Objekt wiederverwendet werden kann, kann die API einen Pool für diese Art von Objekten verwalten. In diesem Fall kann dieser Pool jedoch keinen gleichzeitigen Zugriff unterstützen. Ein Objekt aus dem Pool wechselt vom Status "Abgeschlossen" in den Status "Nicht abgeschlossen" und umgekehrt.


Um die Möglichkeit der Arbeit mit solchen Pools zu unterstützen, wurde .NET Core 2.1 die Schnittstelle IValueTaskSource<TResult> hinzugefügt und die Struktur ValueTask<TResult> erweitert: Jetzt können Objekte dieses Typs nicht nur Objekte vom Task<TResult> TResult oder Task<TResult> , sondern auch Instanzen von IValueTaskSource<TResult> . Die neue Schnittstelle bietet grundlegende Funktionen, mit denen ValueTask<TResult> -Objekte mit IValueTaskSource<TResult> auf dieselbe Weise wie mit Task<TResult> :


 public interface IValueTaskSource<out TResult> { ValueTaskSourceStatus GetStatus(short token); void OnCompleted( Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags); TResult GetResult(short token); } 

GetStatus für die Verwendung in der ValueTask<TResult>.IsCompleted/IsCompletedSuccessfully können Sie feststellen, ob der Vorgang abgeschlossen wurde oder nicht (erfolgreich oder nicht). OnCompleted in ValueTask<TResult> , um einen Rückruf auszulösen. GetResult verwendet, um das Ergebnis GetResult oder eine Ausnahme GetResult .


Es ist unwahrscheinlich, dass sich die meisten Entwickler jemals mit der IValueTaskSource<TResult> -Schnittstelle befassen müssen, weil Wenn asynchrone Methoden zurückgegeben werden, wird sie hinter der ValueTask<TResult> . Die Schnittstelle selbst ist in erster Linie für diejenigen gedacht, die Hochleistungs-APIs entwickeln und unnötige Arbeit mit einem Haufen vermeiden möchten.


In .NET Core 2.1 gibt es mehrere Beispiele für diese Art von API. Die bekannteste davon sind die neuen Überladungen der Methoden Socket.ReceiveAsync und Socket.SendAsync . Z.B:


 public ValueTask<int> ReceiveAsync( Memory<byte> buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default); 

Objekte vom Typ ValueTask<int> werden als Rückgabewert verwendet.
Wenn die Methode synchron beendet wird, gibt sie eine ValueTask<int> mit dem entsprechenden Wert zurück:


 int result = …; return new ValueTask<int>(result); 

Wenn der Vorgang asynchron abgeschlossen wird, wird ein zwischengespeichertes Objekt verwendet, das die Schnittstelle IValueTaskSource<TResult> implementiert:


 IValueTaskSource<int> vts = …; return new ValueTask<int>(vts); 

Die Socket Implementierung unterstützt ein zwischengespeichertes Objekt zum Empfangen und eines zum Senden von Daten, sofern jedes von ihnen ohne Konkurrenz verwendet wird (nein, zum Beispiel das Senden von Wettbewerbsdaten). Diese Strategie reduziert die Menge des zugewiesenen zusätzlichen Speichers, selbst bei asynchroner Ausführung.
Die beschriebene Optimierung von Socket in .NET Core 2.1 hatte positive Auswirkungen auf die Leistung von NetworkStream . Seine Überladung ist die ReadAsync Methode der Stream Klasse:


 public virtual ValueTask<int> ReadAsync( Memory<byte> buffer, CancellationToken cancellationToken); 

delegiert die Arbeit einfach an die Socket.ReceiveAsync Methode. Durch Erhöhen der Effizienz der Socket-Methode im Hinblick auf die Arbeit mit dem Speicher wird die Effizienz der NetworkStream Methode erhöht.


Nicht generische ValueTask


Zuvor habe ich mehrmals festgestellt, dass das ursprüngliche Ziel von ValueTask<T> in .NET Core 2.0 darin bestand, Fälle des synchronen Abschlusses von Methoden mit einem "nicht leeren" Ergebnis zu optimieren. Dies bedeutet, dass keine nicht typisierte ValueTask : Bei synchroner Fertigstellung verwenden Methoden einen Singleton über die Task.CompletedTask Eigenschaft, und die Laufzeit für async Task Methoden wird ebenfalls implizit empfangen.


Mit dem Aufkommen der Möglichkeit, unnötige Zuweisungen zu vermeiden, und mit der asynchronen Ausführung wurde die Notwendigkeit einer nicht typisierten ValueTask wieder relevant. Aus diesem Grund haben wir in .NET Core 2.1 nicht ValueTask und IValueTaskSource . Sie sind Analoga der entsprechenden generischen Typen und werden auf die gleiche Weise verwendet, jedoch für Methoden mit einer leeren ( void ) Rückgabe.


Implementieren Sie IValueTaskSource / IValueTaskSource <T>


Die meisten Entwickler müssen diese Schnittstellen nicht implementieren. Und ihre Umsetzung ist keine leichte Aufgabe. Wenn Sie entscheiden, dass Sie sie selbst implementieren müssen, gibt es in .NET Core 2.1 mehrere Implementierungen, die als Beispiele dienen können:



Um diese Aufgaben zu vereinfachen (Implementierungen von IValueTaskSource / IValueTaskSource<T> ), planen wir, den Typ ManualResetValueTaskSourceCore<TResult> in .NET Core 3.0 ManualResetValueTaskSourceCore<TResult> . Diese Struktur kapselt die gesamte erforderliche Logik. Die Instanz ManualResetValueTaskSourceCore<TResult> kann in einem anderen Objekt verwendet werden, das IValueTaskSource<TResult> und / oder IValueTaskSource , und den größten Teil der Arbeit an dieses Objekt delegieren. Weitere Informationen hierzu finden Sie unter ttps: //github.com/dotnet/corefx/issues/32664.


Das richtige Modell für die Verwendung von ValueTasks


Selbst eine flüchtige Prüfung ValueTask dass ValueTask und ValueTask<TResult> eingeschränkter sind als Task und Task<TResult> . Dies ist normal und sogar wünschenswert, da das Hauptziel darin besteht, auf den Abschluss der asynchronen Ausführung zu warten.


Insbesondere ergeben sich erhebliche Einschränkungen aufgrund der Tatsache, dass ValueTask und ValueTask<TResult> wiederverwendbare Objekte aggregieren können. Im Allgemeinen sollten die folgenden Operationen * ValueTask ausgeführt werden, wenn ValueTask / ValueTask<TResult> * verwendet wird ( lassen Sie mich durch "Never" * umformulieren):


  • Verwenden Sie niemals dasselbe ValueTask / ValueTask<TResult> -Objekt wiederholt

Motivation: Die Instanzen Task und Task<TResult> niemals vom Task<TResult> "abgeschlossen" in den Status "unvollständig". Sie können sie verwenden, um so oft auf das Ergebnis zu warten, wie wir möchten. Nach Abschluss erhalten wir immer das gleiche Ergebnis. Im Gegenteil, da ValueTask / ValueTask<TResult> , können sie als Wrapper für wiederverwendete Objekte fungieren, was bedeutet, dass sich ihr Status ändern kann, weil Der Status wiederverwendeter Objekte ändert sich per Definition - um von "abgeschlossen" zu "unvollständig" und umgekehrt zu wechseln.


  • ValueTask niemals ValueTask / ValueTask&lt;TResult&gt; im Wettbewerbsmodus.

Motivation: Ein verpacktes Objekt erwartet, dass es jeweils nur mit einem Rückruf von einem einzelnen Verbraucher funktioniert, und der Versuch, mit dem erwarteten Wettbewerb zu konkurrieren, kann leicht zu Rennbedingungen und subtilen Programmierfehlern führen. Wettbewerbserwartungen, dies ist eine der oben beschriebenen Optionen für mehrere Erwartungen . Beachten Sie, dass Task / Task<TResult> eine beliebige Anzahl von Wettbewerbserwartungen zulässt.


  • Verwenden .GetAwaiter().GetResult() niemals .GetAwaiter().GetResult() bis der Vorgang abgeschlossen ist .

Motivation: Implementierungen von IValueTaskSource / IValueTaskSource<TResult> sollten das Sperren erst nach Abschluss des Vorgangs unterstützen. Das Blockieren führt in der Tat zu einer Rennbedingung. Es ist unwahrscheinlich, dass dies das erwartete Verhalten des Verbrauchers ist. Mit Task / Task<TResult> können Sie dies tun und so den aufrufenden Thread blockieren, bis der Vorgang abgeschlossen ist.


Was aber, wenn Sie dennoch eine der oben beschriebenen Operationen ausführen müssen und die aufgerufene Methode Instanzen von ValueTask / ValueTask<TResult> ? In solchen Fällen stellt ValueTask / ValueTask<TResult> die Methode .AsTask() . Wenn Sie diese Methode aufrufen, erhalten Sie eine Instanz von Task / Task<TResult> , mit der Sie bereits die erforderliche Operation ausführen können. Die Wiederverwendung des ursprünglichen Objekts nach dem Aufruf von .AsTask() ist nicht zulässig .


: ValueTask / ValueTask<TResult> , ( await ) (, .ConfigureAwait(false) ), .AsTask() , ValueTask / ValueTask<TResult> .


 // Given this ValueTask<int>-returning method… 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(); ... // storing the instance into a local makes it much more likely it'll be misused, // but it could still be ok // BAD: awaits multiple times ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = await vt; int result2 = await vt; // BAD: awaits concurrently (and, by definition then, multiple times) ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); Task.Run(async () => await vt); Task.Run(async () => await vt); // BAD: uses GetAwaiter().GetResult() when it's not known to be done ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = vt.GetAwaiter().GetResult(); 

, "", , ( , ).


ValueTask / ValueTask<TResult> , . , IsCompleted true , ( , ), — false , IsCompletedSuccessfully true . " " , , , , , . await / .AsTask() .Result . , SocketsHttpHandler .NET Core 2.1, .ReadAsync , ValueTask<int> . , , , . , .. . Weil , , , , :


 int bytesRead; { ValueTask<int> readTask = _connection.ReadAsync(buffer); if (readTask.IsCompletedSuccessfully) { bytesRead = readTask.Result; } else { using (_connection.RegisterCancellation()) { bytesRead = await readTask; } } } 

, .. ValueTask<int> , .Result , await , .


API ValueTask / ValueTask<TResult>?


, . Task / ValueTask<TResult> .


, Task / Task<TResult> . , "" / , Task / Task<TResult> . , , ValueTask<TResult> Task<TResult> : , , await Task<TResult> ValueTask<TResult> . , (, API Task Task<bool> ), , , Task ( Task<bool> ). , ValueTask / ValueTask<TResult> . , async-, ValueTask / ValueTask<TResult> , .


, ValueTask / ValueTask<TResult> , :


  1. , API ,
  2. API ,
  3. , , , .

, abstract / virtual , , / ?


Was weiter?


.NET, API, Task / Task<TResult> . , , API c ValueTask / ValueTask<TResult> , . IAsyncEnumerator<T> , .NET Core 3.0. IEnumerator<T> MoveNext , . — IAsyncEnumerator<T> MoveNextAsync . , Task<bool> , , . , , , ( ), , , await foreach -, , MoveNextAsync , ValueTask<bool> . , , , " " , . , C# , .


Source: https://habr.com/ru/post/de465259/


All Articles