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.
Gebundene Parallelität und Ergebnispipeline
Um zu begrenzen, wie viele Vorgänge gleichzeitig beginnen, stellt FSharp.Core 11 bereit Task.parallelLimit. Übergeben Sie Fabriken, die Abbruchtoken akzeptieren, anstatt Aufgaben, die bereits gestartet wurden:
open System.IO
open System.Threading
let readFiles (paths: seq<string>) (ct: CancellationToken) =
paths
|> Seq.map (fun path ->
fun token -> File.ReadAllTextAsync(path, token))
|> Task.parallelLimit 4 ct
Das Ergebnisarray behält die Eingabereihenfolge bei. Ein Fehler fordert den Abbruch gleichgeordneter Arbeiten an, und jeder Vorgang muss sein Token beobachten.
Task.sequential führt Fabriken einzeln aus; Task.parallelDoLimit und Task.sequentialDo anstelle eines Arrays zurückgeben unit .
Informationen Task.mapzu , Task.bindund Task.catch Ergebnispipelinen und übereinstimmenden Async/ValueTask-Hilfsgeräten finden Sie unter "Async" und "Task functions".
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.
Asynchrone Laufzeitexperimente
Generatorautoren können mit .NET asynchronen Laufzeit experimentieren. Ein vom Compiler generierter Zustandscomputer speichert normalerweise den Zustand, der nach einem Warten fortgesetzt werden muss. Bei asynchroner Laufzeit wird stattdessen eine speziell markierte Methode angehalten und fortgesetzt.
Hinweis
Dieser Prototyp verwendet ein F#11.2-Feature. Aktivieren <LangVersion>preview</LangVersion> Sie im .NET 11.0.100 SDK einen Compiler mit runtime-async-Unterstützung. Es benötigt außerdem Laufzeit- und Referenzassemblys, die MethodImplOptions.Async verfügbar sind und verfügbar sind System.Runtime.CompilerServices.AsyncHelpers.Await. Referenzassemblys beschreiben die APIs zur Kompilierungszeit; die ausgeführte .NET Laufzeit muss das Verhalten des Anhaltens/Fortsetzens implementieren.
Die überprüfte Komponentenkombination des Beispiels lautet:
| Component | Version oder Anforderung |
|---|---|
| Compiler | Der F# 11-Compiler, der von FSharp.Compiler.Service 43.13.101-rc2.26473.112bereitgestellt wird. |
| FSharp.Core | Package 11.0.101-rc2.26473.112, using lib/net10.0/FSharp.Core.dll. Dieser Build liefert StateMachineHelpers.__runtimeAsyncReturn; die netstandard2.0 und netstandard2.1 builds nicht. |
| Project Ziel |
net11.0. |
| Laufzeit- und Referenzassemblys |
11.0.0-rc.1.26425.128.NET . |
Diese getesteten Komponenten sind keine einzige SDK-Verteilung. .NET 10.0.12 macht die Referenz-APIs verfügbar, aber die Unterbrechungs-/Fortsetzungsprüfung schlägt bei der Laufzeit mit TypeLoadException. Verwenden Sie oben die überprüfte Laufzeit-/Referenzkombination; Weder Vorschaumodus noch API-Anwesenheitsinformationen unterstützen Laufzeit.
Die folgende Aufrufwebsite verwendet einen anwendungsdefinierten Generator, um eine Datei asynchron zu lesen:
let readManifest (path: string) =
runtimeTask {
let! text = System.IO.File.ReadAllTextAsync path
return text.Length
}
Der Generator verbindet den Berechnungsausdruck mit dem systeminternen Laufzeit-asynchronen Rückgabe- und Await-Hilfsprogramm. Es handelt sich nicht um einen neuen integrierten task Generator:
open System.Threading.Tasks
open System.Runtime.CompilerServices
open Microsoft.FSharp.Core.CompilerServices
type RuntimeTaskBuilder() =
member inline _.Delay([<InlineIfLambda>] code: unit -> 'T) = code
member inline _.Run([<InlineIfLambda>] code: unit -> 'T) : Task<'T> =
StateMachineHelpers.__runtimeAsyncReturn (code())
member inline _.Return(value: 'T) = value
member inline _.Bind(work: Task<'T>, [<InlineIfLambda>] next: 'T -> 'U) =
next (AsyncHelpers.Await work)
let runtimeTask = RuntimeTaskBuilder()
__runtimeAsyncReturn markiert die eingeschlossene Methode für die asynchrone Laufzeit. Innerhalb dieser Methode AsyncHelpers.Await können Sie die nicht abgeschlossene Arbeit anhalten und die Steuerung an den Aufrufer zurückgeben und dann mit dem Ergebnis fortsetzen. Es handelt sich nicht um einen blockierenden .Result Anruf.
Der Compiler überprüft unterstützte Anhaltemuster; byrefs können nicht über eine Aussetzung leben. Das Beispiel definiert Delaynur eine Überladung, RunReturnBind nicht einen Produktions-Generator mit Abbruch, Schleifen oder asynchroner Entsorgung.
Aktivieren Sie das F# 11.2-Feature, bei dem der Inline-Generator definiert und erweitert wird. Aufrufer einer bereits kompilierten readManifest API benötigen sie nicht. Gewöhnliche task Ausdrücke behalten ihre Implementierung bei. Verwenden Sie AsyncHelpers.Await nur innerhalb der asynchronen Laufzeitmethoden. Siehe die asynchrone Laufzeitspezifikation.
Das vollständige Beispielprojekt enthält einen Konsoleneinstiegspunkt. Führen Sie dotnet run -- manifest.txt aus dem Verzeichnis mit kompatiblen Tools und einer zu lesenden Datei aus. Eine Datei, die genau manifest, ohne eine nachfolgende Neulinie enthält, erzeugt 8. Die Konsoleneinstiegspunktblöcke für den Abschluss; readManifest selbst gibt zurück Task<int>.