39 Stimmen

Wie funktioniert das Async-Feature von F# wirklich?

Ich versuche zu lernen, wie async und let! in F# funktionieren. Alle Dokumentationen, die ich gelesen habe, erscheinen verwirrend. Was ist der Sinn, einen async-Block mit Async.RunSynchronously auszuführen? Ist das async oder synchron? Sieht nach einem Widerspruch aus.

Die Dokumentation sagt, dass Async.StartImmediate im aktuellen Thread läuft. Wenn es im selben Thread läuft, sieht es für mich nicht sehr asynchron aus... Oder vielleicht sind Asyncs mehr wie Koroutinen als Threads. Wenn ja, wann geben sie zurück und fort?

Zitiere MS-Dokumentation:

Die Zeile des Codes, die let! verwendet, startet die Berechnung und dann wird der Thread ausgesetzt bis das Ergebnis verfügbar ist, wobei die Ausführung fortgesetzt wird.

Wenn der Thread auf das Ergebnis wartet, warum sollte ich es verwenden? Sieht aus wie ein einfacher Funktionsaufruf.

Und was macht Async.Parallel? Es empfängt eine Sequenz von Async<'T>. Warum nicht eine Sequenz von einfachen Funktionen, die parallel ausgeführt werden sollen?

Ich glaube, hier fehlt mir etwas sehr Grundlegendes. Ich denke, nachdem ich das verstanden habe, wird die gesamte Dokumentation und die Beispiele sinnvoll werden.

37voto

Brian Punkte 115257

Ein paar Dinge.

Erstens ist der Unterschied zwischen

let resp = req.GetResponse()

und

let! resp = req.AsyncGetReponse()

der, dass für die wahrscheinlich Hunderte von Millisekunden (eine Ewigkeit für die CPU), in denen die Webanfrage 'in der Luft ist', ersteres einen Thread verwendet (blockiert auf E/A), während letzteres keine Threads verwendet. Dies ist der häufigste 'Vorteil' von async: Sie können nicht blockierende E/A schreiben, die keine Threads verschwendet, die auf Festplattenrotationen oder Netzwerkanfragen warten. (Im Gegensatz zu den meisten anderen Sprachen sind Sie nicht gezwungen, die Steuerung umzukehren und Dinge in Rückrufe zu zerlegen.)

Zweitens wird Async.StartImmediate einen Async auf dem aktuellen Thread starten. Ein üblicher Einsatz ist bei einer GUI, wenn Sie eine GUI-App haben, die z.B. die Benutzeroberfläche aktualisieren möchte (z.B. irgendwo "Loading..." anzeigen) und dann im Hintergrund arbeiten möchte (etwas von der Festplatte laden oder ähnliches) und dann zum Vordergrund-UI-Thread zurückkehren möchte, um die Benutzeroberfläche zu aktualisieren, wenn die Operation abgeschlossen ist ("fertig!"). StartImmediate ermöglicht einem Async, die Benutzeroberfläche am Anfang der Operation zu aktualisieren und den SynchronizationContext zu erfassen, so dass am Ende der Operation zur GUI zurückgekehrt werden kann, um eine abschließende Aktualisierung der Benutzeroberfläche durchzuführen.

Weiterhin wird Async.RunSynchronously selten verwendet (eine These besagt, dass Sie es höchstens einmal in einer App aufrufen). Im Extremfall, wenn Sie Ihr gesamtes Programm async geschrieben hätten, würden Sie in der "main"-Methode RunSynchronously aufrufen, um das Programm auszuführen und auf das Ergebnis zu warten (z.B. um das Ergebnis in einer Konsolen-App auszudrucken). Dies blockiert einen Thread, daher ist es in der Regel nur am obersten Punkt des Async-Teils Ihres Programms, an der Grenze zurück zur synchronen Aktivität, sinnvoll. (Der fortgeschrittenere Benutzer zieht möglicherweise StartWithContinuations vor - RunSynchronously ist sozusagen der "einfache Hack", um von async zu sync zurückzukehren.)

Zu guter Letzt führt Async.Parallel eine Gabel-Join-Parallelität durch. Sie könnten eine ähnliche Funktion schreiben, die einfach Funktionen anstelle von asyncs verwendet (wie im TPL), aber der typische Anwendungsfall in F# sind parallele I/O-gebundene Berechnungen, die bereits Async-Objekte sind, daher ist dies die am häufigsten nützliche Signatur. (Für CPU-gebundene Parallelität könnten Sie Asyncs verwenden, aber genauso gut TPL verwenden.)

12voto

Yin Zhu Punkte 16798

Der Verwendungszweck von async besteht darin, die Anzahl der verwendeten Threads zu reduzieren.

Betrachten Sie das folgende Beispiel:

let fetchUrlSync url = 
    let req = WebRequest.Create(Uri url)
    use resp = req.GetResponse()
    use stream = resp.GetResponseStream()
    use reader = new StreamReader(stream)
    let contents = reader.ReadToEnd()
    contents 

let sites = ["http://www.bing.com";
             "http://www.google.com";
             "http://www.yahoo.com";
             "http://www.search.com"]

// führen Sie die fetchUrlSync-Funktion parallel aus 
let pagesSync = sites |> PSeq.map fetchUrlSync  |> PSeq.toList

Der obige Code ist das, was Sie tun möchten: eine Funktion definieren und parallel ausführen. Warum brauchen wir hier also async?

Betrachten wir etwas Großes. Zum Beispiel, wenn die Anzahl der Websites nicht 4 beträgt, sondern sagen wir, 10.000! Dann braucht es 10.000 Threads, um sie parallel auszuführen, was eine enorme Ressourcenbelastung darstellt.

Im Vergleich dazu in async:

let fetchUrlAsync url =
    async { let req =  WebRequest.Create(Uri url)
            use! resp = req.AsyncGetResponse()
            use stream = resp.GetResponseStream()
            use reader = new StreamReader(stream)
            let contents = reader.ReadToEnd()
            return contents }
let pagesAsync = sites |> Seq.map fetchUrlAsync |> Async.Parallel |> Async.RunSynchronously

Wenn der Code in use! resp = req.AsyncGetResponse() ist, gibt der aktuelle Thread auf und seine Ressourcen können für andere Zwecke verwendet werden. Wenn die Antwort also in 1 Sekunde zurückkommt, könnte Ihr Thread diese 1 Sekunde nutzen, um andere Dinge zu verarbeiten. Andernfalls ist der Thread blockiert und verschwendet Ressourcen für 1 Sekunde.

Selbst wenn Sie 10000 Webseiten gleichzeitig auf eine asynchrone Weise herunterladen, ist die Anzahl der Threads auf eine kleine Anzahl beschränkt.

Ich denke, Sie sind kein .Net/C#-Programmierer. Das Async-Tutorial setzt normalerweise voraus, dass man .Net kennt und wie man asynchrones IO in C# programmiert (eine Menge Code). Die Magie des Async-Konstrukts in F# besteht nicht darin, dass es parallel ausgeführt wird. Denn einfache Parallelität könnte durch andere Konstrukte realisiert werden, z.B. ParallelFor in der .Net-Parallelerweiterung. Die asynchrone IO ist jedoch komplexer, wie Sie sehen, der Thread gibt seine Ausführung auf, wenn das IO fertig ist, muss das IO seinen übergeordneten Thread aufwecken. Hier wird die Async-Magie eingesetzt: In mehreren Zeilen präziser Code können Sie sehr komplexe Steuerungen ausführen.

9voto

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.

4voto

Dave Glassborow Punkte 3015

Viele Details in den anderen Antworten, aber als Anfänger bin ich über die Unterschiede zwischen C# und F# gestolpert.

F# Async-Blöcke sind ein Rezept dafür, wie der Code ausgeführt werden soll, und nicht tatsächlich eine Anweisung, ihn noch auszuführen.

Sie bauen Ihr Rezept auf, vielleicht in Kombination mit anderen Rezepten (z.B. Async.Parallel). Erst dann fordern Sie das System auf, es auszuführen, und das können Sie entweder auf dem aktuellen Thread (z.B. Async.StartImmediate) oder in einer neuen Aufgabe tun, oder auf verschiedene andere Arten.

Es ist also eine Trennung dessen, was Sie tun möchten, von der Person, die es tun sollte.

Das C#-Modell wird oft als 'Hot Tasks' bezeichnet, weil die Tasks für Sie beim Definieren gestartet werden, im Gegensatz zu den 'Cold Task'-Modellen in F#.

1voto

Gabe Punkte 82268

Die Idee hinter let! und Async.RunSynchronously ist, dass es manchmal eine asynchrone Aktivität gibt, deren Ergebnisse Sie benötigen, bevor Sie fortfahren können. Beispielsweise hat die Funktion "eine Webseite herunterladen" möglicherweise keine synchrone Entsprechung, daher benötigen Sie eine Möglichkeit, sie synchron auszuführen. Oder wenn Sie ein Async.Parallel haben, haben Sie möglicherweise Hunderte von Aufgaben, die alle gleichzeitig ablaufen, aber Sie möchten, dass sie alle abgeschlossen sind, bevor Sie fortfahren.

Soweit ich sehen kann, wird Async.StartImmediate verwendet, weil Sie eine Berechnung haben, die auf dem aktuellen Thread (vielleicht einem UI-Thread) ausgeführt werden muss, ohne diesen zu blockieren. Verwendet es Koroutinen? Ich denke, man könnte es so nennen, obwohl es in .Net keinen allgemeinen Koroutinenmechanismus gibt.

Warum erfordert Async.Parallel eine Sequenz von Async<'T>? Wahrscheinlich, weil es eine Möglichkeit ist, Async<'T>-Objekte zu komponieren. Sie könnten problemlos Ihre eigene Abstraktion erstellen, die nur mit einfachen Funktionen funktioniert (oder einer Kombination aus einfachen Funktionen und Asyncs), aber es wäre nur eine Bequemlichkeitsfunktion.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X