Warum sollte man Task <T> über ValueTask <T> in C # verwenden?

168

Ab C # 7.0 können asynchrone Methoden ValueTask <T> zurückgeben. Die Erklärung besagt, dass es verwendet werden sollte, wenn wir ein zwischengespeichertes Ergebnis haben oder asynchron über synchronen Code simulieren. Ich verstehe jedoch immer noch nicht, was das Problem bei der Verwendung von ValueTask immer ist oder warum async / await nicht von Anfang an mit einem Werttyp erstellt wurde. Wann würde ValueTask den Job nicht erledigen?

Stilgar
quelle
7
Ich vermute, dass dies mit den Vorteilen zu tun hat, dass ValueTask<T>(in Bezug auf Zuweisungen) für tatsächlich asynchrone Vorgänge keine Ergebnisse erzielt werden (da in diesem Fall ValueTask<T>noch eine Heap-Zuweisung erforderlich ist). Es geht auch darum Task<T>, viel andere Unterstützung in Bibliotheken zu haben.
Jon Skeet
3
@ JonSkeet vorhandene Bibliotheken sind ein Problem, aber dies wirft die Frage auf, ob Task von Anfang an ValueTask gewesen sein sollte. Die Vorteile bestehen möglicherweise nicht, wenn es für tatsächliche asynchrone Inhalte verwendet wird, aber ist es schädlich?
Stilgar
8
Siehe github.com/dotnet/corefx/issues/4708#issuecomment-160658188 für mehr Weisheit, als ich vermitteln könnte :)
Jon Skeet
3
@ JoelMueller die Handlung verdickt sich :)
Stilgar

Antworten:

245

Aus den API-Dokumenten (Hervorhebung hinzugefügt):

Methoden geben möglicherweise eine Instanz dieses Wertetyps zurück, wenn es wahrscheinlich ist, dass das Ergebnis ihrer Operationen synchron verfügbar ist und wenn erwartet wird, dass die Methode so häufig aufgerufen wird, dass die Kosten für die Zuweisung einer neuen Methode Task<TResult>für jeden Aufruf unerschwinglich sind.

Es gibt Kompromisse bei der Verwendung von a ValueTask<TResult>anstelle von a Task<TResult>. Während a beispielsweise ValueTask<TResult>dazu beitragen kann, eine Zuordnung zu vermeiden, wenn das erfolgreiche Ergebnis synchron verfügbar ist, enthält es auch zwei Felder, während a Task<TResult>als Referenztyp ein einzelnes Feld ist. Dies bedeutet, dass ein Methodenaufruf zwei Datenfelder anstelle von einem zurückgibt, dh mehr zu kopierende Daten. Dies bedeutet auch, dass asyncdie Zustandsmaschine für diese asyncMethode größer ist, wenn eine Methode, die eine dieser Methoden zurückgibt, innerhalb einer Methode erwartet wird, da die Struktur aus zwei Feldern anstelle einer einzelnen Referenz gespeichert werden muss.

Ferner kann für andere Zwecke als über das Ergebnis einer asynchronen Operation raubend await, ValueTask<TResult>kann zu einer gewundenen Programmiermodell führen, die tatsächlich wiederum kann auf mehr Zuteilungen führen. Stellen Sie sich beispielsweise eine Methode vor, die entweder a Task<TResult>mit einer zwischengespeicherten Aufgabe als gemeinsames Ergebnis oder a zurückgeben kann ValueTask<TResult>. Wenn der Konsument des Ergebnisses es als verwenden möchte, beispielsweise um es Task<TResult>in Methoden wie Task.WhenAllund zu verwenden Task.WhenAny, ValueTask<TResult>müsste das zuerst in eine Task<TResult>Verwendung konvertiert werden AsTask, was zu einer Zuordnung führt, die vermieden worden wäre, wenn ein Cache verwendet Task<TResult>worden wäre an erster Stelle.

Daher sollte die Standardauswahl für jede asynchrone Methode die Rückgabe eines Taskoder sein Task<TResult>. Nur wenn sich die Leistungsanalyse als sinnvoll erweist, sollte ValueTask<TResult>stattdessen eine verwendet werden Task<TResult>.

Stephen Cleary
quelle
7
@MattThomas: Es wird eine einzelne TaskZuordnung gespeichert (die heutzutage klein und billig ist), jedoch auf Kosten der Vergrößerung der vorhandenen Zuordnung des Anrufers und der Verdoppelung des Rückgabewerts (Auswirkungen auf die Registerzuweisung). Obwohl es eine klare Wahl für ein gepuffertes Leseszenario ist, würde ich nicht empfehlen, es standardmäßig auf alle Schnittstellen anzuwenden.
Stephen Cleary
1
Richtig, entweder Taskoder ValueTaskkann als synchroner Rückgabetyp (mit Task.FromResult) verwendet werden. Aber es gibt immer noch einen Wert (heh), ValueTaskwenn Sie etwas haben, von dem Sie erwarten , dass es synchron ist. ReadByteAsyncein klassisches Beispiel sein. Ich glaube, es ValueTask wurde hauptsächlich für die neuen "Kanäle" (Low-Level-Byte-Streams) erstellt, die möglicherweise auch im ASP.NET-Kern verwendet werden, wo die Leistung wirklich wichtig ist.
Stephen Cleary
1
Oh, ich weiß, dass lol, ich habe mich nur gefragt, ob Sie diesem speziellen Kommentar etwas hinzuzufügen haben;)
Julealgon
2
Ist diese PR schalten Sie die Waage über zu bevorzugen ValueTask? (ref: blog.marcgravell.com/2019/08/… )
stuartd
2
@stuartd: Im Moment würde ich immer noch empfehlen, Task<T>als Standard zu verwenden. Dies liegt nur daran, dass die meisten Entwickler mit den Einschränkungen nicht vertraut sind ValueTask<T>(insbesondere die Regel "Nur einmal verbrauchen" und die Regel "Keine Blockierung"). Das heißt, wenn alle Entwickler in Ihrem Team sind gut mit ValueTask<T>, dann würde ich ein empfehlen Teamebene Leitlinie vorzieht ValueTask<T>.
Stephen Cleary
104

Ich verstehe jedoch immer noch nicht, was das Problem bei der Verwendung von ValueTask ist

Strukturtypen sind nicht frei. Das Kopieren von Strukturen, die größer als die Größe einer Referenz sind, kann langsamer sein als das Kopieren einer Referenz. Das Speichern von Strukturen, die größer als eine Referenz sind, benötigt mehr Speicher als das Speichern einer Referenz. Strukturen, die größer als 64 Bit sind, werden möglicherweise nicht registriert, wenn eine Referenz registriert werden kann. Die Vorteile eines geringeren Sammeldrucks dürfen die Kosten nicht überschreiten.

Leistungsprobleme sollten mit einer technischen Disziplin angegangen werden. Machen Sie Ziele, messen Sie Ihren Fortschritt anhand von Zielen und entscheiden Sie dann, wie Sie das Programm ändern, wenn die Ziele nicht erreicht werden. Messen Sie dabei, ob Ihre Änderungen tatsächlich Verbesserungen sind.

warum async / await nicht von Anfang an mit einem Werttyp erstellt wurde.

awaitwurde zu C # hinzugefügt, lange nachdem der Task<T>Typ bereits vorhanden war. Es wäre etwas pervers gewesen, einen neuen Typ zu erfinden, wenn es ihn bereits gab. Und awaiter hat sehr viele Design-Iterationen durchlaufen, bevor er sich für die entschieden hat, die 2012 ausgeliefert wurde. Das Perfekte ist der Feind des Guten; Es ist besser, eine Lösung zu liefern, die gut mit der vorhandenen Infrastruktur zusammenarbeitet, und dann bei Bedarf der Benutzer später Verbesserungen vorzunehmen.

Ich stelle außerdem fest, dass die neue Funktion, vom Benutzer bereitgestellte Typen als Ausgabe einer vom Compiler generierten Methode zuzulassen, ein erhebliches Risiko und einen erheblichen Testaufwand mit sich bringt. Wenn die einzigen Dinge, die Sie zurückgeben können, nichtig oder eine Aufgabe sind, muss das Testteam kein Szenario berücksichtigen, in dem ein absolut verrückter Typ zurückgegeben wird. Das Testen eines Compilers bedeutet, nicht nur herauszufinden, welche Programme wahrscheinlich geschrieben werden, sondern auch, welche Programme geschrieben werden können , da der Compiler alle legalen Programme kompilieren soll, nicht nur alle sinnvollen Programme. Das ist teuer.

Kann jemand erklären, wann ValueTask den Job nicht erledigen würde?

Der Zweck der Sache ist eine verbesserte Leistung. Es macht den Job nicht, wenn es die Leistung nicht messbar und signifikant verbessert. Es gibt keine Garantie dafür.

Eric Lippert
quelle
23

ValueTask<T>ist keine Untermenge von Task<T>, es ist eine Obermenge .

ValueTask<T>ist eine diskriminierte Vereinigung von T und a Task<T>, wodurch es frei von Zuweisungen ReadAsync<T>ist, einen verfügbaren T-Wert synchron zurückzugeben (im Gegensatz zur Verwendung Task.FromResult<T>, bei der eine Task<T>Instanz zugewiesen werden muss). ValueTask<T>ist zu erwarten, so dass der größte Teil des Verbrauchs von Instanzen nicht von einem zu unterscheiden ist Task<T>.

ValueTask ist eine Struktur und ermöglicht das Schreiben von asynchronen Methoden, die keinen Speicher zuweisen, wenn sie synchron ausgeführt werden, ohne die API-Konsistenz zu beeinträchtigen. Stellen Sie sich eine Schnittstelle mit einer Task-Rückgabemethode vor. Jede Klasse, die diese Schnittstelle implementiert, muss eine Aufgabe zurückgeben, auch wenn sie synchron ausgeführt wird (hoffentlich mit Task.FromResult). Sie können natürlich zwei verschiedene Methoden auf der Schnittstelle haben, eine synchrone und eine asynchrone, aber dies erfordert zwei verschiedene Implementierungen, um "Synchronisieren über Asynchronisieren" und "Asynchronisieren über Synchronisieren" zu vermeiden.

Sie können also eine Methode schreiben, die entweder asynchron oder synchron ist, anstatt für jede Methode eine ansonsten identische Methode zu schreiben. Sie könnten es überall verwenden, Task<T>aber es würde oft nichts hinzufügen.

Nun, es fügt eines hinzu: Es fügt dem Aufrufer ein implizites Versprechen hinzu, dass die Methode tatsächlich die zusätzlichen Funktionen verwendet, die sie ValueTask<T>bereitstellt. Ich persönlich bevorzuge die Auswahl von Parameter- und Rückgabetypen, die dem Anrufer so viel wie möglich mitteilen. Geben Sie nicht zurück, IList<T>wenn die Aufzählung keine Zählung liefern kann. kehre nicht zurück, IEnumerable<T>wenn es geht. Ihre Verbraucher sollten keine Dokumentation nachschlagen müssen, um zu wissen, welche Ihrer Methoden vernünftigerweise synchron aufgerufen werden können und welche nicht.

Ich sehe zukünftige Designänderungen dort nicht als überzeugendes Argument. Im Gegenteil: Wenn eine Methode ihre Semantik ändert, sollte sie den Build unterbrechen, bis alle Aufrufe entsprechend aktualisiert wurden. Wenn dies als unerwünscht angesehen wird (und glauben Sie mir, ich bin mit dem Wunsch einverstanden, den Build nicht zu brechen), ziehen Sie die Versionierung der Benutzeroberfläche in Betracht.

Dies ist im Wesentlichen das, wofür starkes Tippen gedacht ist.

Wenn einige der Programmierer, die in Ihrem Shop asynchrone Methoden entwerfen, keine fundierten Entscheidungen treffen können, kann es hilfreich sein, jedem dieser weniger erfahrenen Programmierer einen leitenden Mentor zuzuweisen und eine wöchentliche Codeüberprüfung durchzuführen. Wenn sie falsch raten, erklären Sie, warum dies anders gemacht werden sollte. Es ist Overhead für die Senioren, aber es wird die Junioren viel schneller auf den neuesten Stand bringen, als sie nur in die Tiefe zu werfen und ihnen eine willkürliche Regel zu geben, der sie folgen müssen.

Wenn der Typ, der die Methode geschrieben hat, nicht weiß, ob sie synchron aufgerufen werden kann, wer auf der Erde tut das?!

Wenn Sie so viele unerfahrene Programmierer haben, die asynchrone Methoden schreiben, rufen dieselben Leute sie auch auf? Sind sie qualifiziert, selbst herauszufinden, welche sicher als asynchron bezeichnet werden können, oder wenden sie eine ähnlich willkürliche Regel an, wie sie diese Dinge nennen?

Das Problem hierbei sind nicht Ihre Rückgabetypen, sondern Programmierer, denen Rollen zugewiesen werden, für die sie nicht bereit sind. Das muss aus einem bestimmten Grund geschehen sein, daher kann es sicher nicht trivial sein, das Problem zu beheben. Es zu beschreiben ist sicherlich keine Lösung. Aber nach einer Möglichkeit zu suchen, das Problem am Compiler vorbei zu schleichen, ist auch keine Lösung.

15ee8f99-57ff-4f92-890c-b56153
quelle
4
Wenn ich sehe, dass eine Methode zurückkehrt ValueTask<T>, gehe ich davon aus, dass der Typ, der die Methode geschrieben hat, dies getan hat, da die Methode tatsächlich die hinzugefügte Funktionalität verwendet ValueTask<T>. Ich verstehe nicht, warum Sie es für wünschenswert halten, dass alle Ihre Methoden denselben Rückgabetyp haben. Was ist das Ziel dort?
15ee8f99-57ff-4f92-890c-b56153
2
Ziel 1 wäre es, eine Änderung der Implementierung zu ermöglichen. Was ist, wenn ich das Ergebnis jetzt nicht zwischenspeichere, sondern später den Zwischenspeichercode hinzufüge? Ziel 2 wäre es, eine klare Richtlinie für ein Team zu haben, in dem nicht alle Mitglieder verstehen, wann ValueTask nützlich ist. Tun Sie es einfach immer, wenn die Verwendung keinen Schaden anrichtet.
Stilgar
6
Meiner Ansicht nach ist das fast so, als würden alle Ihre Methoden zurückgegeben, objectweil Sie vielleicht eines Tages möchten, dass sie intstatt zurückkehren string.
15ee8f99-57ff-4f92-890c-b56153
3
Vielleicht, aber wenn Sie die Frage stellen "Warum würden Sie jemals eine Zeichenfolge anstelle eines Objekts zurückgeben?", Kann ich sie leicht beantworten und auf die mangelnde Typensicherheit hinweisen. Die Gründe, ValueTask nicht überall zu verwenden, scheinen viel subtiler zu sein.
Stilgar
3
@Stilgar Eines würde ich sagen: Subtile Probleme sollten Sie mehr beunruhigen als offensichtliche.
15ee8f99-57ff-4f92-890c-b56153
20

Es gibt einige Änderungen in .Net Core 2.1 . Ab .net Core 2.1 kann ValueTask nicht nur die synchron abgeschlossenen Aktionen darstellen, sondern auch die abgeschlossene Asynchronisierung. Zusätzlich erhalten wir nicht generischen ValueTaskTyp.

Ich werde Stephen Toub einen Kommentar hinterlassen , der sich auf Ihre Frage bezieht:

Wir müssen die Leitlinien noch formalisieren, aber ich gehe davon aus, dass dies für die öffentliche API-Oberfläche in etwa so sein wird:

  • Aufgabe bietet die meiste Benutzerfreundlichkeit.

  • ValueTask bietet die meisten Optionen zur Leistungsoptimierung.

  • Wenn Sie eine Schnittstelle / virtuelle Methode schreiben, die andere überschreiben, ist ValueTask die richtige Standardauswahl.

  • Wenn Sie erwarten, dass die API auf heißen Pfaden verwendet wird, auf denen Zuordnungen eine Rolle spielen, ist ValueTask eine gute Wahl.

  • Wenn die Leistung nicht kritisch ist, verwenden Sie standardmäßig Task, da dies bessere Garantien und Benutzerfreundlichkeit bietet.

Aus Sicht der Implementierung werden viele der zurückgegebenen ValueTask-Instanzen weiterhin von Task unterstützt.

Die Funktion kann nicht nur im .net Core 2.1 verwendet werden. Sie können es mit dem System.Threading.Tasks.Extensions- Paket verwenden.

unsafePtr
quelle
1
Mehr von Stephen heute: blogs.msdn.microsoft.com/dotnet/2018/11/07/…
Mike Cheel