Здесь много хороших ответов, но я подумал, что под другим углом зрения задаю вопрос: как на самом деле работает асинхронность F #?
В отличие от async/await
в C # F # разработчики могут реализовать собственную версию Async
. Это может быть отличным способом узнать, как работает Async
.
(Для интересующихся исходный код Async
можно найти здесь: https://github.com/Microsoft/visualfsharp/blob/fsharp4/src/fsharp/FSharp.Core/control.fs)
В качестве основного строительного блока для наших рабочих процессов DIY мы определяем:
type DIY<'T> = ('T->unit)->unit
Это функция, которая принимает другую функцию (называемую продолжением), которая вызывается, когда готов результат типа 'T
. Это позволяет DIY<'T>
запускать фоновую задачу, не блокируя вызывающий поток. Когда результат готов, вызывается продолжение, позволяющее продолжить вычисление.
Строительный блок F # Async
немного сложнее, поскольку он также включает в себя отмены и продолжения исключений, но по сути это так.
Для поддержки синтаксиса рабочего процесса F # нам нужно определить выражение для вычисления (https://msdn.microsoft.com/en-us/library/dd233182.aspx).. Хотя это довольно продвинутая функция F #, она также является одной из самых удивительных функций F #. Две самые важные операции для определить return
& bind
, которые используются F # для объединения наших DIY<_>
строительных блоков в агрегированные DIY<_>
строительные блоки.
adaptTask
используется для адаптации Task<'T>
в DIY<'T>
.
startChild
позволяет запускать несколько одновременных DIY<'T>
, обратите внимание, что он не запускает новые потоки для этого, но повторно использует вызывающий поток.
Без лишних слов, вот пример программы:
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
Вывод программы должен выглядеть примерно так:
Starting workflow
Waiting for key
Result is: (706L, 1, 2, 3)
При запуске программы обратите внимание, что Waiting for key
печатается сразу, поскольку поток консоли не блокируется от запуска рабочего процесса. Примерно через 700 мс результат печатается.
Надеюсь, это было интересно некоторым разработчикам F #