Freigeben über


Aufgabenausdrücke

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:

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.

Siehe auch