Viele gute Antworten hier, aber ich dachte, ich nehme einen anderen Ansatz zur Frage: Wie funktioniert async
in F# wirklich?
Im Gegensatz zu async/await
in C# können F#-Entwickler tatsächlich ihre eigene Version von Async
implementieren. Dies kann eine großartige Möglichkeit sein, zu lernen, wie Async
funktioniert.
(Für die Interessierten kann der Quellcode zu Async
hier gefunden werden: https://github.com/Microsoft/visualfsharp/blob/fsharp4/src/fsharp/FSharp.Core/control.fs)
Als unser grundlegendes Baumaterial für unsere DIY-Workflows definieren wir:
type DIY<'T> = ('T->unit)->unit
Dies ist eine Funktion, die eine andere Funktion akzeptiert (die sogenannte Fortsetzung), die aufgerufen wird, wenn das Ergebnis des Typs 'T
bereit ist. Dies ermöglicht es DIY<'T>
, eine Hintergrundaufgabe zu starten, ohne den Aufrufer-Thread zu blockieren. Wenn das Ergebnis bereit ist, wird die Fortsetzung aufgerufen, was die Berechnung ermöglicht fortsetzen.
Der F# Async
-Baustein ist etwas komplizierter, da er auch Abbruch- und Ausnahme-Fortsetzungen enthält, aber im Grunde genommen ist das so.
Um die F#-Workflow-Syntax zu unterstützen, müssen wir einen Berechnungsausdruck definieren (https://msdn.microsoft.com/en-us/library/dd233182.aspx). Obwohl dies eine ziemlich fortgeschrittene F#-Funktionalität ist, ist sie auch eine der erstaunlichsten Funktionen von F#. Die zwei wichtigsten Operationen, die definiert werden müssen, sind return
& bind
, die von F# verwendet werden, um unsere DIY<_>
-Bausteine in aggregierte DIY<_>
-Bausteine zu kombinieren.
adaptTask
wird verwendet, um eine Task<'T>
in ein DIY<'T>
zu adaptieren. startChild
ermöglicht das Starten mehrerer gleichzeitiger DIY<'T>
, wobei zu beachten ist, dass es keine neuen Threads startet, um dies zu tun, sondern den Aufrufer-Thread wiederverwendet.
Und nun zum Beispielprogramm:
pre>open System open System.Diagnostics open System.Threading open System.Threading.Tasks // Unser Do It Yourself Async-Workflow ist eine Funktion, die eine Fortsetzung ('T->unit) akzeptiert. // Die Fortsetzung wird aufgerufen, wenn das Ergebnis des Workflows bereit ist. // Dies kann sofort oder nach einer Weile geschehen, wichtig ist, dass // wir den Aufrufer-Thread nicht blockieren, der dann damit fortfahren kann, nützlichen Code auszuführen. type DIY<'T> = ('T->unit)->unit // Um let!, do! usw. zu unterstützen, implementieren wir einen Berechnungsausdruck. // Die beiden wichtigsten Operationen sind returnValue/bind, aber delay ist auch im Allgemeinen // gut zu implementieren. module DIY = // returnValue wird aufgerufen, wenn Entwickler return x in einem Workflow verwendet. // returnValue übergibt v sofort an die Fortsetzung. let returnValue (v : 'T) : DIY<'T> = fun a -> a v // bind wird aufgerufen, wenn Entwickler let!/do! x in einem Workflow verwendet // bind verbindet zwei DIY-Workflows miteinander let bind (t : DIY<'T>) (fu : 'T->DIY<'U>) : DIY<'U> = fun a -> let aa tv = let u = fu tv u a t aa let delay (ft : unit->DIY<'T>) : DIY<'T> = fun a -> let t = ft () t a // startet einen DIY-Workflow als Unterworkflow // Die Funktionsweise besteht darin, dass der Workflow ausgeführt wird // was möglicherweise eine verzögerte Operation ist. Aber startChild // sollte immer sofort abgeschlossen werden, damit etwas zurückgegeben wird // gibt einen DIY-Workflow zurück // postProcess prüft, ob das Kind einen Wert berechnet hat // das heißt, rv hat einen Wert und wenn wir eine Berechnung bereit haben // um den Wert zu empfangen (rca hat einen Wert). // Wenn dies zutrifft, rufen Sie ca mit v auf let startChild (t : DIY<'T>) : DIY> = fun a -> let l = obj() let rv = ref None let rca = ref None let postProcess () = match !rv, !rca with | Some v, Some ca -> ca v rv := None rca := None | _ , _ -> () let receiver v = lock l <| fun () -> rv := Some v postProcess () t receiver let child : DIY<'T> = fun ca -> lock l <| fun () -> rca := Some ca postProcess () a child let runWithContinuation (t : DIY<'T>) (f : 'T -> unit) : unit = t f // Passt eine Aufgabe als DIY-Workflow an let adaptTask (t : Task<'T>) : DIY<'T> = fun a -> let action = Action> (fun t -> a t.Result) ignore <| t.ContinueWith action // Da C#-Generika keine Task erlauben, benötigen wir // eine spezielle Überlastung für die Einheitentask. let adaptUnitTask (t : Task) : DIY = fun a -> let action = Action (fun t -> a ()) ignore <| t.ContinueWith action type DIYBuilder() = member x.Return(v) = returnValue v member x.Bind(t,fu) = bind t fu member x.Delay(ft) = delay ft let diy = DIY.DIYBuilder() open DIY [] let main argv = let delay (ms : int) = adaptUnitTask <| Task.Delay ms let delayedValue ms v = diy { do! delay ms return v } let complete = diy { let sw = Stopwatch () sw.Start () // Da wir diese Tasks gleichzeitig ausführen // sollte die Zeit, die dies dauert, ungefähr 700 ms betragen. let! cd1 = startChild <| delayedValue 100 1 let! cd2 = startChild <| delayedValue 300 2 let! cd3 = startChild <| delayedValue 700 3 let! d1 = cd1 let! d2 = cd2 let! d3 = cd3 sw.Stop () return sw.ElapsedMilliseconds,d1,d2,d3 } printfn "Workflow starten" runWithContinuation complete (printfn "Das Ergebnis ist: %A") printfn "Auf Taste warten" ignore <| Console.ReadKey () 0
Die Ausgabe des Programms sollte ungefähr so aussehen:
Workflow starten
Auf Taste warten
Das Ergebnis ist: (706L, 1, 2, 3)
Wenn das Programm ausgeführt wird, beachten Sie, dass Auf Taste warten
sofort gedruckt wird, da der Konsolen-Thread nicht daran gehindert wird, den Workflow zu starten. Nach ungefähr 700ms wird das Ergebnis gedruckt.
Ich hoffe, das war für einige F#-Entwickler interessant.