Taskausdrücke
In diesem Artikel wird die Unterstützung in F# von Taskausdrücken beschrieben, die mit asynchronen Ausdrücken vergleichbar sind, jedoch das direkte Erstellen von .NET-Tasks ermöglichen. Wie asynchrone Ausdrücke führen Taskausdrücke Code asynchron aus, d. h. ohne die Ausführung anderer Arbeiten zu blockieren.
Asynchroner Code wird normalerweise mit asynchronen Ausdrücken erstellt. Die Verwendung von Taskausdrücken wird bevorzugt, wenn eine umfangreiche Zusammenarbeit mit .NET-Bibliotheken erfolgt, die .NET-Tasks erstellen oder nutzen. Taskausdrücke können auch die Leistung und das Debuggen verbessern. Taskausdrücke weisen jedoch einige Einschränkungen auf, die weiter unten im Artikel beschrieben werden.
Syntax
task { expression }
In der vorherigen Syntax wird die durch expression
dargestellte Berechnung so eingerichtet, dass sie als .NET-Task ausgeführt wird. Der Task wird unmittelbar nach der Ausführung dieses Codes gestartet und im aktuellen Thread ausgeführt, bis der erste asynchrone Vorgang ausgeführt wird (z. B. ein asynchroner Standbymodus, asynchrone E/A oder ein anderer primitiver asynchroner Vorgang). Als Ausdruckstyp wird Task<'T>
verwendet, wobei 'T
der Typ ist, der vom Ausdruck zurückgegeben wird, wenn das Schlüsselwort return
verwendet wird.
Binden mithilfe von let!
In einem Taskausdruck sind einige Ausdrücke und Vorgänge synchron, andere asynchron. Wenn Sie auf das Ergebnis eines asynchronen Vorgangs warten, verwenden Sie let!
anstelle einer normalen let
-Bindung . Der Effekt von let!
besteht darin, die Ausführung für andere Berechnungen oder Threads zu ermöglichen, während die Berechnung ausgeführt wird. Nachdem die rechte Seite der let!
-Bindung zurückgegeben wurde, wird die Ausführung für den Rest des Tasks fortgesetzt.
Anhand des folgenden Codes wird der Unterschied zwischen let
und let!
deutlich. Die Codezeile, in der let
verwendet wird, erstellt lediglich einen Task als Objekt, auf das Sie später warten können, indem Sie z. B. task.Wait()
oder task.Result
verwenden. Die Codezeile, in der let!
verwendet wird, startet den Task und wartet auf das Ergebnis.
// let just stores the result as a task.
let (result1 : Task<int>) = stream.ReadAsync(buffer, offset, count, cancellationToken)
// let! completes the asynchronous operation and returns the data.
let! (result2 : int) = stream.ReadAsync(buffer, offset, count, cancellationToken)
task { }
-Ausdrücke in F# können auf die folgenden Arten von asynchronen Vorgänge warten:
- .NET-Tasks, Task<TResult> und der nicht generische Task
- .NET-Werttasks, ValueTask<TResult> und der nicht generische ValueTask
- Asynchrone Berechnungen in F#
Async<T>
- Alle Objekte, die dem in F# RFC FS-1097 angegebenen „GetAwaiter“-Muster entsprechen.
return
-Ausdrücke
Innerhalb von Taskausdrücken wird return expr
verwendet, um das Ergebnis eines Tasks zurückzugeben.
return!
-Ausdrücke
Innerhalb von Taskausdrücken wird return! expr
verwendet, um das Ergebnis eines Tasks zurückzugeben. Dies entspricht der Verwendung von let!
und der anschließenden sofortigen Rückgabe des Ergebnisses.
Ablaufsteuerung
Taskausdrücke können die Ablaufsteuerungskonstrukte for .. in .. do
, while .. do
, try .. with ..
, try .. finally ..
, if .. then .. else
und if .. then ..
enthalten. Diese können wiederum weitere Taskkonstrukte enthalten, mit Ausnahme der with
- und finally
-Handler, die synchron ausgeführt werden. Wenn Sie ein asynchrones try .. finally ..
-Konstrukt benötigen, verwenden Sie eine use
-Bindung in Kombination mit einem Objekt vom Typ IAsyncDisposable
.
use
- und use!
-Bindungen
Innerhalb von Taskausdrücken können use
-Bindungen an Werte vom Typ IDisposable oder IAsyncDisposable gebunden werden. Für letzteres wird der Bereinigungsvorgang asynchron ausgeführt.
Neben let!
können Sie auch use!
verwenden, um asynchrone Bindungen durchzuführen. Der Unterschied zwischen let!
und use!
ist der gleiche wie der zwischen let
und use
. Bei use!
wird das Objekt beim Schließen des aktuellen Bereichs verworfen. Beachten Sie, dass use!
in F# 6 nicht zulässt, dass ein Wert auf NULL initialisiert wird, obwohl dies mit use
möglich wäre.
Werttasks
Werttasks sind Strukturen, die dazu dienen, Zuordnungen in der taskbasierten Programmierung zu vermeiden. Ein Werttask ist ein kurzlebiger Wert, der mithilfe von .AsTask()
in einen echten Task umgewandelt wird.
Verwenden Sie zum Erstellen eines Werttasks aus einem Taskausdruck |> ValueTask<ReturnType>
oder |> ValueTask
. Beispiel:
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
Hinzufügen von Abbruchtoken und Abbruchprüfungen
Im Gegensatz zu asynchronen Ausdrücken in F# übergeben Taskausdrücke kein Abbruchtoken und führen keine impliziten Abbruchprüfungen durch. Wenn Ihr Code ein Abbruchtoken erfordert, sollten Sie das Abbruchtoken als Parameter angeben. Beispiel:
open System.Threading
let someTaskCode (cancellationToken: CancellationToken) =
task {
cancellationToken.ThrowIfCancellationRequested()
printfn $"continuing..."
}
Wenn es möglich sein soll, dass Ihr Code ordnungsgemäß abgebrochen werden kann, müssen Sie sorgfältig prüfen, ob das Abbruchtoken an alle .NET-Bibliotheksvorgänge übergeben wird, die einen Abbruch unterstützen. Beispielsweise verfügt Stream.ReadAsync
über mehrere Überladungen, von denen eine ein Abbruchtoken akzeptiert. Wenn Sie diese Überladung nicht verwenden, kann dieser spezifische asynchrone Lesevorgang nicht abgebrochen werden.
Hintergrundaufgaben
Sofern vorhanden werden .NET-Tasks standardmäßig mithilfe von SynchronizationContext.Current geplant. Dadurch können Tasks als kooperative Interleave-Agents dienen, die auf einem Benutzeroberflächenthread ausgeführt werden, ohne die Benutzeroberfläche zu blockieren. Sind diese nicht vorhanden, werden Taskfortsetzungen für den .NET-Threadpool geplant.
In der Praxis ist es häufig wünschenswert, dass Bibliothekscode, der Tasks generiert, den Synchronisierungskontext ignoriert und stattdessen bei Bedarf zum .NET-Threadpool wechselt. Dies lässt sich mithilfe von backgroundTask { }
erreichen:
backgroundTask { expression }
Ein Hintergrundtask ignoriert alle SynchronizationContext.Current
im folgenden Sinne: Wenn er in einem Thread mit SynchronizationContext.Current
ungleich NULL gestartet wird, wechselt er mithilfe von Task.Run
zu einem Hintergrundthread im Threadpool. Wenn er in einem Thread mit null SynchronizationContext.Current
gestartet wird, wird er für diesen Thread ausgeführt.
Hinweis
In der Praxis bedeutet dies, dass Aufrufe von ConfigureAwait(false)
in F#-Taskcode in der Regel nicht benötigt werden. Stattdessen sollten Tasks, die im Hintergrund ausgeführt werden sollen, mit backgroundTask { ... }
erstellt werden. Jede äußere Taskbindung an einen Hintergrundtask wird nach Abschluss des Hintergrundtasks erneut mit SynchronizationContext.Current
synchronisiert.
Einschränkungen von Tasks in Bezug auf Endaufrufe
Im Gegensatz zu asynchronen Ausdrücken in F# unterstützen Taskausdrücke keine Endaufrufe. Das heißt, wenn return!
ausgeführt wird, wird der aktuelle Task als Warten auf den Task registriert, dessen Ergebnis zurückgegeben wird. Das bedeutet, dass rekursive Funktionen und Methoden, die mit Taskausdrücken implementiert werden, ungebundene Taskketten erstellen können, und diese können ungebundene Stapel oder Heaps verwenden. Beachten Sie z. B. folgenden Code:
let rec taskLoopBad (count: int) : Task<string> =
task {
if count = 0 then
return "done!"
else
printfn $"looping..., count = {count}"
return! taskLoopBad (count-1)
}
let t = taskLoopBad 10000000
t.Wait()
Dieser Codierungsstil sollte nicht mit Taskausdrücken verwendet werden. Damit wird eine Kette mit 100.000.000 Tasks erstellt und eine StackOverflowException
verursacht. Wenn für jeden Schleifenaufruf ein asynchroner Vorgang hinzugefügt wird, verwendet der Code im Wesentlichen einen ungebundenen Heap. Überlegen Sie, ob es sinnvoll ist, eine explizite Schleife zu verwenden, z. B.:
let taskLoopGood (count: int) : Task<string> =
task {
for i in count .. 1 do
printfn $"looping... count = {count}"
return "done!"
}
let t = taskLoopGood 10000000
t.Wait()
Wenn asynchrone Endaufrufe erforderlich sind, verwenden Sie einen asynchronen F#-Ausdruck, der Endaufrufe unterstützt. Beispiel:
let rec asyncLoopGood (count: int) =
async {
if count = 0 then
return "done!"
else
printfn $"looping..., count = {count}"
return! asyncLoopGood (count-1)
}
let t = asyncLoopGood 1000000 |> Async.StartAsTask
t.Wait()
Taskimplementierung
Tasks werden mit fortsetzbarem Code implementiert, einem neuen Feature in F# 6. Tasks werden vom F#-Compiler in „Fortsetzbarer Zustand-Maschinen“ kompiliert. Diese werden in der RFC Resumable Code und in F# Compiler Community Session ausführlich beschrieben.