Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
In diesem Artikel wird die Unterstützung in F# für Aufgabenausdrücke beschrieben, die mit asynchronen Ausdrücken vergleichbar sind, aber ihnen ermöglichen, .NET-Aufgaben direkt zu erstellen. Wie asynchrone Ausdrücke führen Aufgabenausdrü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 Aufgabenausdrücken wird bevorzugt, wenn Sie umfassend mit .NET-Bibliotheken zusammenarbeiten, die .NET-Aufgaben erstellen oder nutzen. Aufgabenausdrücke können auch die Leistung und die Debugerfahrung verbessern. Aufgabenausdrücke enthalten jedoch einige Einschränkungen, die weiter unten im Artikel beschrieben werden.
Syntax
task { expression }
In der vorherigen Syntax wird die durch die dargestellte expression Berechnung eingerichtet, um als .NET-Task ausgeführt zu werden. Die Aufgabe wird sofort gestartet, nachdem dieser Code ausgeführt wurde und im aktuellen Thread ausgeführt wird, bis der erste asynchrone Vorgang ausgeführt wird (z. B. asynchroner Standbymodus, asynchroner E/A- oder anderer primitiver asynchroner Vorgang). Der Typ des Ausdrucks ist Task<'T>, wobei 'T der Typ, der vom Ausdruck zurückgegeben wird, wenn das return Schlüsselwort verwendet wird.
Binden mithilfe von Let!
In einem Aufgabenausdruck sind einige Ausdrücke und Vorgänge synchron, und einige sind asynchron. Wenn Sie auf das Ergebnis eines asynchronen Vorgangs warten, verwenden Sie letanstelle einer normalen let! Bindung . Der Effekt let! besteht darin, die Ausführung für andere Berechnungen oder Threads zu ermöglichen, während die Berechnung ausgeführt wird. Nach der Rückkehr der rechten Seite der let! Bindung wird die Ausführung der übrigen Aufgabe fortgesetzt.
Der folgende Code zeigt den Unterschied zwischen let und let!. Die Codezeile, die let verwendet, erstellt einfach eine Aufgabe als Objekt, auf das Sie später warten können, indem Sie z. B. task.Wait() oder task.Result verwenden. Die Codezeile, die let! verwendet, um die Aufgabe zu starten und auf das Ergebnis zu warten.
// 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)
F# task { } -Ausdrücke können auf die folgenden Arten asynchroner Vorgänge warten:
- .NET-Aufgaben Task<TResult> und die nicht generischen Task.
- .NET-Wertaufgaben ValueTask<TResult> und die nicht generischen ValueTask.
- F# asynchrone Berechnungen
Async<T>. - Jedes Objekt, das auf das in F# RFC FS-1097 angegebene "GetAwaiter"-Muster folgt.
return-Ausdrücke
Innerhalb von Aufgabenausdrücken wird return expr verwendet, um das Ergebnis einer Aufgabe zurückzugeben.
return!-Ausdrücke
Innerhalb von Aufgabenausdrücken wird return! expr verwendet, um das Ergebnis einer anderen Aufgabe zurückzugeben. Sie entspricht der Verwendung let! und gibt dann das Ergebnis sofort zurück.
Kontrollfluss
Aufgabenausdrücke können die Kontrollflusskonstrukte for .. in .. do, while .. do, try .. with .., try .. finally .., if .. then .. else und if .. then .. umfassen. Diese können wiederum weitere Aufgabenkonstrukte enthalten, mit Ausnahme der withfinally Handler, die synchron ausgeführt werden. Wenn Sie eine asynchrone try .. finally .. benötigen, verwenden Sie eine use-Bindung in Kombination mit einem Objekt vom Typ IAsyncDisposable.
use und use! Bindungen
Innerhalb von Aufgabenausdrücken können use-Bindungen an Werte vom Typ IDisposable oder IAsyncDisposable gebunden werden. Für letzteres wird der Entsorgungsbereinigungsvorgang asynchron ausgeführt.
Zusätzlich zu let! können Sie use! verwenden, um asynchrone Bindungen auszuführen. Der Unterschied zwischen let! und use! ist identisch mit dem Unterschied zwischen let und use. Für use! wird das Objekt am Ende des aktuellen Geltungsbereichs freigegeben. Beachten Sie, dass in F# 6 kein Wert auf `null` initialisiert werden kann, auch wenn `use!` dies erlaubt.
open System
open System.IO
open System.Security.Cryptography
task {
// use IDisposable
use httpClient = new Net.Http.HttpClient()
// use! Task<IDisposable>
use! exampleDomain = httpClient.GetAsync "https://example.com/data.enc"
// use IDisposable
use aes = Aes.Create()
aes.KeySize <- 256
aes.GenerateIV()
aes.GenerateKey()
// do! Task
do! File.WriteAllTextAsync("key.iv.txt", $"Key: {Convert.ToBase64String aes.Key}\nIV: {Convert.ToBase64String aes.IV}")
// use IAsyncDisposable
use outputStream = File.Create "secret.enc"
// use IDisposable
use encryptor = aes.CreateEncryptor()
// use IAsyncDisposable
use cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)
// do! Task
do! exampleDomain.Content.CopyToAsync cryptoStream
}
Wertaufgaben
Wertaufgaben werden verwendet, um Zuordnungen in der aufgabenbasierten Programmierung zu vermeiden. Eine Wertaufgabe ist ein kurzlebiger Wert, der durch die Verwendung von .AsTask() in eine echte Aufgabe umgewandelt wird.
Um eine Wertaufgabe aus einem Aufgabenausdruck zu erstellen, verwenden |> ValueTask<ReturnType> oder |> ValueTask. Beispiel:
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
and! Bindungen (beginnend mit F# 10)
Innerhalb von Aufgabenausdrücken ist es möglich, gleichzeitig auf mehrere asynchrone Vorgänge (Task<'T>, ValueTask<'T>Async<'T> usw.) zu warten. Vergleichen:
// We'll wait for x to resolve and then for y to resolve. Overall execution time is sum of two execution times.
let getResultsSequentially() =
task {
let! x = getX()
let! y = getY()
return x, y
}
// x and y will be awaited concurrently. Overall execution time is the time of the slowest operation.
let getResultsConcurrently() =
task {
let! x = getX()
and! y = getY()
return x, y
}
Hinzufügen von Abbruch-Token und Überprüfungen
Im Gegensatz zu F#-Async-Ausdrücken übergeben Task-Ausdrü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 Sie beabsichtigen, den Code ordnungsgemäß abzubrechen, überprüfen Sie sorgfältig, ob Sie das Abbruchtoken an alle .NET-Bibliotheksvorgänge übergeben, die den Abbruch unterstützen. Zum Beispiel hat Stream.ReadAsync mehrere Überladungen, von denen eine ein Abbruchtoken akzeptiert. Wenn Sie diese Überladung nicht verwenden, kann dieser spezifische asynchrone Lesevorgang nicht abgebrochen werden.
Hintergrundaufgaben
Standardmäßig werden .NET-Aufgaben geplant, wenn SynchronizationContext.Current vorhanden ist. Dadurch können Aufgaben als kooperative, ineinandergreifende Agenten dienen, die im Benutzeroberflächenthread ausgeführt werden, ohne die Benutzeroberfläche zu blockieren. Falls nicht vorhanden, werden Aufgabenfortsetzungen an den .NET-Threadpool übergeben.
In der Praxis ist es häufig wünschenswert, dass Bibliothekscode, der Aufgaben generiert, den Synchronisierungskontext ignoriert und stattdessen bei Bedarf zum .NET-Threadpool wechselt. Sie können dies mit backgroundTask { } erreichen:
backgroundTask { expression }
Eine Hintergrundaufgabe ignoriert alle SynchronizationContext.Current in folgendem Sinne: Wenn sie in einem Thread mit nicht NULL SynchronizationContext.Current gestartet wird, wechselt sie mit Task.Run zu einem Hintergrundthread im Threadpool. Wenn er in einem Thread mit NULL SynchronizationContext.Currentgestartet wird, wird er für denselben Thread ausgeführt.
Hinweis
In der Praxis bedeutet dies, dass ConfigureAwait(false)-Aufrufe in der Regel nicht im F#-Aufgabencode erforderlich sind. Aufgaben, die im Hintergrund ausgeführt werden sollen, sollten mit backgroundTask { ... } erstellt werden. Jede äußere Aufgabenbindung an eine Hintergrundaufgabe wird nach Abschluss der Hintergrundaufgabe erneut synchronisiert SynchronizationContext.Current .
Einschränkungen von Aufgaben in Bezug auf Tailcalls
Im Gegensatz zu F#-asynchronen Ausdrücken unterstützen Task-Ausdrücke keine Tail-Calls. Das heißt, wenn return! ausgeführt wird, wird der aktuelle Vorgang als wartend auf den Vorgang registriert, dessen Ergebnis zurückgegeben wird. Dies bedeutet, dass rekursive Funktionen und Methoden, die mit Aufgabenausdrücken implementiert werden, ungebundene Aufgabenketten erstellen können, und diese können ungebundene Stapel oder Heaps verwenden. Betrachten Sie z. B. den 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 Aufgabenausdrücken verwendet werden – er erstellt eine Kette von 100000000 Aufgaben und verursacht eine StackOverflowException. Wenn für jeden Schleifenaufruf ein asynchroner Vorgang hinzugefügt wird, verwendet der Code im Wesentlichen einen ungebundenen Heap. Erwägen Sie, diesen Code zu ändern, damit eine explizite Schleife verwendet wird, 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 Tailcalls erforderlich sind, verwenden Sie einen asynchronen Ausdruck in F#, der Tailcalls 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()
Aufgabenimplementierung
Aufgaben werden mit resumable Code implementiert, einem neuen Feature in F# 6. Aufgaben werden vom F#-Compiler in "Resumable State Machines" kompiliert. Diese werden im Resumable Code RFC und in einer F#-Compiler-Community-Sitzung ausführlich beschrieben.