¿Cómo funciona realmente la asincronización de F#?
asynchronous (7)
El uso de async es para guardar la cantidad de subprocesos en uso.
Vea el siguiente ejemplo:
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"]
// execute the fetchUrlSync function in parallel
let pagesSync = sites |> PSeq.map fetchUrlSync |> PSeq.toList
El código anterior es lo que desea hacer: definir una función y ejecutarla en paralelo. Entonces, ¿por qué necesitamos asincronización aquí?
Consideremos algo grande. Por ejemplo, si la cantidad de sitios no es 4, ¡digamos 10.000! Luego, se necesitan 10.000 hilos para ejecutarlos en paralelo, lo que representa un gran costo de recursos.
Mientras está en modo asincrónico:
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
Cuando el código está en use! resp = req.AsyncGetResponse()
use! resp = req.AsyncGetResponse()
, el hilo actual se abandona y su recurso podría usarse para otros fines. Si la respuesta vuelve en 1 segundo, tu hilo podría usar este 1 segundo para procesar otras cosas. De lo contrario, el hilo está bloqueado, desperdiciando el recurso de hilo durante 1 segundo.
Así que incluso si está descargando 10000 páginas web en paralelo de forma asíncrona, el número de subprocesos está limitado a un número pequeño.
Creo que no eres un programador de .Net / C #. El tutorial asincrónico asume que se conoce .Net y cómo programar IO asíncrona en C # (mucho código). La magia de la construcción Async en F # no es paralela. Debido a que el paralelo simple podría ser realizado por otras construcciones, por ejemplo, ParallelFor en la extensión paralela .Net. Sin embargo, el IO asíncrono es más complejo, como verá, el hilo renuncia a su ejecución, cuando el IO finaliza, el IO necesita activar su hilo padre. Aquí es donde se usa la magia asíncrona: en varias líneas de código conciso, puedes hacer un control muy complejo.
¡Estoy intentando aprender cómo async
y let!
trabajar en F #. Todos los documentos que he leído parecen confusos. ¿De qué sirve ejecutar un bloque asíncrono con Async.RunSynchronously? ¿Esto es asincrónico o sincronizado? Parece una contradicción.
La documentación dice que Async.StartImmediate se ejecuta en el hilo actual. Si se ejecuta en el mismo hilo, no se ve muy asíncrono para mí ... O tal vez asyncs son más coroutines en lugar de hilos. Si es así, ¿cuándo ceden un cuarto?
Citando documentos MS:
La línea de código que usa let! inicia el cálculo y luego el hilo se suspende hasta que el resultado esté disponible, en cuyo punto la ejecución continúa.
Si el hilo espera el resultado, ¿por qué debería usarlo? Parece una simple llamada de función antigua.
¿Y qué hace Async.Parallel? Recibe una secuencia de Async <''T>. ¿Por qué no una secuencia de funciones simples para ser ejecutadas en paralelo?
Creo que me estoy perdiendo algo muy básico aquí. Supongo que después de entender eso, toda la documentación y las muestras comenzarán a tener sentido.
En un bloque asíncrono, puede tener algunas operaciones sincrónicas y algunas asíncronas, por ejemplo, puede tener un sitio web que muestre el estado del usuario de varias maneras, por lo que puede mostrar si tienen facturas pendientes en breve, cumpleaños por venir y deberes. Ninguno de estos se encuentra en la misma base de datos, por lo que su aplicación realizará tres llamadas por separado. Es posible que desee realizar las llamadas en paralelo, de modo que cuando finalice la más lenta, puede juntar los resultados y mostrarlos, por lo que el resultado final será que la visualización se basa en la más lenta. No le importa el orden en que vuelvan, solo quiere saber cuándo se reciben los tres.
Para finalizar mi ejemplo, puede que quiera hacer el trabajo sincrónicamente para crear la interfaz de usuario para mostrar esta información. Entonces, al final, usted quería que se obtuvieran estos datos y se mostrara la IU, las partes donde el orden no importa se hace en paralelo, y donde las cuestiones de orden se pueden hacer de forma síncrona.
Puede hacer esto como tres hilos, pero luego debe hacer un seguimiento y reanudar el hilo original cuando termine el tercero, pero es más trabajo, es más fácil que el .NET Framework se encargue de esto.
Gran cantidad de detalles en las otras respuestas, pero como principiante me tropecé con las diferencias entre C # y F #.
Los bloques F asincrónicos son una receta de cómo debe ejecutarse el código, y no una instrucción para ejecutarlo todavía.
Usted construye su receta, tal vez combinándola con otras recetas (por ejemplo, Async.Parallel). Solo entonces le pide al sistema que lo ejecute, y puede hacerlo en el hilo actual (por ejemplo, Async.StartImmediate) o en una nueva tarea, o en varias otras formas.
Entonces, es un desacoplamiento de lo que quieres hacer con quién debería hacerlo.
El modelo C # a menudo se llama ''Tareas activas'' porque las tareas se inician para usted como parte de su definición, frente a los modelos F # ''Tarea fría''.
La idea detrás de let!
y Async.RunSynchronously
es que a veces tiene una actividad asincrónica que necesita los resultados antes de poder continuar. Por ejemplo, la función "descargar una página web" puede no tener un equivalente síncrono, por lo que necesita alguna forma de ejecutarla de forma síncrona. O si tiene una Async.Parallel
, puede tener cientos de tareas todas sucediendo al mismo tiempo, pero desea que todas se completen antes de continuar.
Por lo que puedo decir, la razón por la que usaría Async.StartImmediate
es que tiene algunos cálculos que necesita ejecutar en el hilo actual (quizás un hilo de interfaz de usuario) sin bloquearlo. ¿Utiliza coroutines? Supongo que podría llamarlo así, aunque no hay un mecanismo de corotine general en .Net.
Entonces, ¿por qué Async.Parallel
requiere una secuencia de Async<''T>
? Probablemente porque es una forma de componer objetos Async<''T>
. Podrías crear fácilmente tu propia abstracción que funcione con funciones simples (o una combinación de funciones simples y Async
s, pero solo sería una función de conveniencia.
Muchas buenas respuestas aquí, pero creo que tomo un ángulo diferente a la pregunta: ¿Cómo funciona realmente la función asíncrona de F #?
A diferencia de async/await
en C # F # los desarrolladores pueden implementar su propia versión de Async
. Esta puede ser una excelente forma de aprender cómo funciona Async
.
(Para los interesados, el código fuente de Async
se puede encontrar aquí: https://github.com/Microsoft/visualfsharp/blob/fsharp4/src/fsharp/FSharp.Core/control.fs )
Como nuestro componente fundamental para nuestros flujos de trabajo de bricolaje, definimos:
type DIY<''T> = (''T->unit)->unit
Esta es una función que acepta otra función (llamada la continuación) que se llama cuando el resultado del tipo ''T
está listo. Esto permite que DIY<''T>
inicie una tarea en segundo plano sin bloquear el hilo de llamada. Cuando el resultado está listo, la continuación se llama permitiendo que el cálculo continúe.
El building block F # Async
es un poco más complicado, ya que también incluye cancelaciones y continuación de excepciones, pero básicamente esto es todo.
Para soportar la sintaxis del flujo de trabajo F #, necesitamos definir una expresión de cálculo ( https://msdn.microsoft.com/en-us/library/dd233182.aspx ). Si bien esta es una característica avanzada de F #, también es una de las características más sorprendentes de F #. Las dos operaciones más importantes para definir son return
& bind
que son utilizadas por F # para combinar nuestros bloques de construcción DIY_ DIY<_>
en bloques de construcción DIY<_>
agregados DIY<_>
.
adaptTask
se usa para adaptar una Task<''T>
a un DIY<''T>
. startChild
permite iniciar varios simulatenous DIY<''T>
, tenga en cuenta que no inicia nuevos subprocesos para hacerlo, pero reutiliza el hilo de llamada.
Sin más preámbulos aquí está el programa de ejemplo:
open System
open System.Diagnostics
open System.Threading
open System.Threading.Tasks
// Our Do It Yourself Async workflow is a function accepting a continuation (''T->unit).
// The continuation is called when the result of the workflow is ready.
// This may happen immediately or after awhile, the important thing is that
// we don''t block the calling thread which may then continue executing useful code.
type DIY<''T> = (''T->unit)->unit
// In order to support let!, do! and so on we implement a computation expression.
// The two most important operations are returnValue/bind but delay is also generally
// good to implement.
module DIY =
// returnValue is called when devs uses return x in a workflow.
// returnValue passed v immediately to the continuation.
let returnValue (v : ''T) : DIY<''T> =
fun a ->
a v
// bind is called when devs uses let!/do! x in a workflow
// bind binds two DIY workflows together
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
// starts a DIY workflow as a subflow
// The way it works is that the workflow is executed
// which may be a delayed operation. But startChild
// should always complete immediately so in order to
// have something to return it returns a DIY workflow
// postProcess checks if the child has computed a value
// ie rv has some value and if we have computation ready
// to receive the value (rca has some value).
// If this is true invoke ca with v
let startChild (t : DIY<''T>) : DIY<DIY<''T>> =
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
// Adapts a task as a DIY workflow
let adaptTask (t : Task<''T>) : DIY<''T> =
fun a ->
let action = Action<Task<''T>> (fun t -> a t.Result)
ignore <| t.ContinueWith action
// Because C# generics doesn''t allow Task<void> we need to have
// a special overload of for the unit Task.
let adaptUnitTask (t : Task) : DIY<unit> =
fun a ->
let action = Action<Task> (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
[<EntryPoint>]
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 ()
// Since we are executing these tasks concurrently
// the time this takes should be roughly 700ms
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 "Starting workflow"
runWithContinuation complete (printfn "Result is: %A")
printfn "Waiting for key"
ignore <| Console.ReadKey ()
0
La salida del programa debería ser algo como esto:
Starting workflow
Waiting for key
Result is: (706L, 1, 2, 3)
Al ejecutar el programa, tenga en cuenta que la Waiting for key
se imprime inmediatamente ya que el subproceso de la Consola no está bloqueado al iniciar el flujo de trabajo. Después de aproximadamente 700 ms, se imprime el resultado.
Espero que esto haya sido interesante para algunos desarrolladores de F #
Recientemente, hice una breve descripción de las funciones en el módulo Async: here . Quizás ayude.
Unas pocas cosas.
Primero, la diferencia entre
let resp = req.GetResponse()
y
let! resp = req.AsyncGetReponse()
es que probablemente durante cientos de milisegundos (una eternidad a la CPU) donde la solicitud web está "en el mar", la primera está utilizando un hilo (bloqueado en E / S), mientras que el último está utilizando hilos cero . Esta es la ''ganancia'' más común para la sincronización: puede escribir E / S sin bloqueo que no desperdicie ningún subproceso en espera de que los discos duros giren o las solicitudes de red vuelvan. (A diferencia de la mayoría de los otros lenguajes, no está obligado a realizar una inversión de control y factorizar las cosas en devoluciones de llamadas).
En segundo lugar, Async.StartImmediate
comenzará una asincronización en el hilo actual. Un uso típico es con una GUI, tiene alguna aplicación GUI que quiere, por ejemplo, actualizar la UI (por ejemplo, para decir "cargando ..." en algún lugar), y luego hacer algún trabajo de fondo (cargar algo del disco o lo que sea), y luego volver al hilo de interfaz de usuario en primer plano para actualizar la interfaz de usuario una vez completada ("hecho!"). StartImmediate
habilita una StartImmediate
para actualizar la UI al inicio de la operación y para capturar el SynchronizationContext
para que al final de la operación pueda regresar a la GUI para realizar una actualización final de la UI.
A continuación, Async.RunSynchronously
se usa raramente (una tesis es que la llames como máximo una vez en cualquier aplicación). En el límite, si escribió todo el programa como sincronización, en el método "principal" llamaría RunSynchronously
para ejecutar el programa y esperar el resultado (por ejemplo, para imprimir el resultado en una aplicación de consola). Esto bloquea un hilo, por lo que normalmente solo es útil en la ''parte superior'' de la parte asincrónica de tu programa, en el límite con elementos de sincronización. (El usuario más avanzado puede preferir StartWithContinuations
- RunSynchronously
es un poco el "truco fácil" para pasar de la sincronización a la sincronización).
Finalmente, Async.Parallel
realiza el paralelismo de unión por horquilla. Podría escribir una función similar que solo tome funciones en lugar de async
(como cosas en el TPL), pero el punto dulce típico en F # son los cálculos paralelos de E / S vinculados, que ya son objetos asíncronos, por lo que este es el más común firma útil. (Para el paralelismo con CPU, puede usar asyncs, pero también podría usar TPL).