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:

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.Currentungleich NULL gestartet wird, wechselt er mithilfe von Task.Run zu einem Hintergrundthread im Threadpool. Wenn er in einem Thread mit null SynchronizationContext.Currentgestartet 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.

Weitere Informationen