F#的异步如何真正起作用?

时间:2010-08-22 03:00:44

标签: f# asynchronous

我正在尝试了解asynclet!在F#中的运作方式。 我读过的所有文档都让人感到困惑。 使用Async.RunSynchron运行异步块有什么意义?这是异步还是同步?看起来像是一个矛盾。

文档说Async.StartImmediate在当前线程中运行。如果它在同一个线程中运行,它对我来说看起来并不是异步......或者asyncs更像是协程而不是线程。如果是这样的话,他们什么时候屈服回来?

引用MS文档:

  

使用let的代码行!开始计算,然后线程被挂起   直到结果可用,此时继续执行。

如果线程等待结果,我为什么要使用它?看起来像普通的旧函数调用。

Async.Parallel有什么作用?它接收一系列Async<'T>。为什么不能并行执行一系列普通函数?

我想我在这里缺少一些非常基本的东西。我想在我理解之后,所有文档和样本都将开始有意义。

7 个答案:

答案 0 :(得分:32)

一些事情。

首先,

之间的区别
let resp = req.GetResponse()

let! resp = req.AsyncGetReponse()

是网络请求“在海上”的可能数百毫秒(CPU的永恒),前者使用一个线程(在I / O上阻塞),而后者使用个线程。这是异步最常见的“胜利”:您可以编写非阻塞I / O,不会浪费任何线程等待硬盘转动或网络请求返回。 (与大多数其他语言不同,您不会被迫进行控制反转并将事物纳入回调中。)

其次,Async.StartImmediate启动当前线程的异步。典型的用途是使用GUI,你有一些想要的GUI应用程序,例如更新UI(例如说“加载......”某处),然后做一些后台工作(从磁盘或其他任何东西加载),然后返回到前台UI线程以在完成时更新UI(“完成!” )。 StartImmediate允许异步在操作开始时更新UI并捕获SynchronizationContext,以便在操作结束时可以返回GUI以执行UI的最终更新。

接下来,很少使用Async.RunSynchronously(一篇论文是你在任何应用程序中最多调用一次)。在限制中,如果您将整个程序编写为异步,则在“main”方法中,您可以调用RunSynchronously来运行程序并等待结果(例如,在控制台应用程序中打印出结果)。这会阻塞一个线程,因此它通常只在程序的异步部分的“顶部”有用,在边界上有同步的东西。 (更高级的用户可能更喜欢StartWithContinuations - RunSynchronously有点“容易入侵”从异步返回同步。)

最后,Async.Parallel执行fork-join并行操作。您可以编写一个类似于函数而不是async的函数(就像TPL中的东西一样),但F#中的典型最佳位置是并行I / O绑定计算,它们已经是异步对象,所以这个是最常用的签名。 (对于CPU绑定的并行性,您可以使用asyncs,但也可以使用TPL。)

答案 1 :(得分:12)

async的用法是保存使用中的线程数。

请参阅以下示例:

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

上面的代码是你想要做的:定义一个函数并并行执行。那么为什么我们需要异步呢?

让我们考虑一下大事。例如。如果网站的数量不是4,而是说10,000!然后需要10,000个线程并行运行它们,这是一个巨大的资源成本。

在异步中:

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

当代码在use! resp = req.AsyncGetResponse()时,当前线程被放弃,其资源可用于其他目的。如果响应在1秒后回复,那么你的线程可以使用这1秒来处理其他东西。否则线程被阻塞,浪费线程资源1秒钟。

因此,即使您以异步方式并行下载10000个网页,线程数也仅限于少量。

我认为你不是.Net / C#程序员。异步教程通常假定人们知道.Net以及如何在C#中编写异步IO(很多代码)。 F#中Async构造的神奇之处不在于并行。因为简单的并行可以通过其他结构实现,例如ParallelFor在.Net并行扩展中。但是,异步IO更复杂,因为您看到线程放弃执行,当IO完成时,IO需要唤醒其父线程。这是异步魔术用于的地方:在几行简洁代码中,您可以进行非常复杂的控制。

答案 2 :(得分:9)

这里有很多好的答案,但我认为我对这个问题采取了不同的角度:F#的异步是如何起作用的?

与C#F#中的async/await不同,开发人员实际上可以实现自己的Async版本。这可以是了解Async如何运作的好方法。

(感兴趣的是Async的源代码可以在这里找到:https://github.com/Microsoft/visualfsharp/blob/fsharp4/src/fsharp/FSharp.Core/control.fs

作为我们DIY工作流程的基本构建块,我们定义:

type DIY<'T> = ('T->unit)->unit

这是一个接受另一个函数(称为continuation)的函数,当函数'T的结果准备就绪时调用该函数。这允许DIY<'T>启动后台任务而不会阻塞调用线程。当结果准备好时,将调用continuation以允许继续计算。

F#Async构建块有点复杂,因为它还包括取消和异常延续,但基本上就是这样。

为了支持F#工作流语法,我们需要定义一个计算表达式(https://msdn.microsoft.com/en-us/library/dd233182.aspx)。虽然这是一个相当先进的F#功能,但它也是F#最神奇的功能之一。要定义的两个最重要的操作是return&amp; 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会立即打印,因为控制台线程未被阻止启动工作流程。大约700ms后打印结果。

我希望这对一些F#devs来说很有意思

答案 3 :(得分:6)

最近,我简要概述了异步模块中的功能:here。也许它会有所帮助。

答案 4 :(得分:2)

其他答案中有很多细节,但是在我初学的时候,我被C#和F#之间的差异搞砸了。

F#异步块是配方,代码应该如何运行,而不是实际运行它的指令。

您可以建立您的食谱,可能与其他食谱(例如Async.Parallel)结合使用。只有这样,您才要求系统运行它,您可以在当前线程(例如Async.StartImmediate)或新任务或其他各种方式上执行此操作。

所以这是你想做什么与谁应该做的脱钩。

C#模型通常被称为“热门任务”,因为任务是作为其定义的一部分为您启动的,而不是F#“冷任务”模型。

答案 5 :(得分:1)

let!Async.RunSynchronously背后的想法是,有时您需要一个异步活动,在继续之前需要结果。例如,“下载网页”功能可能没有同步等效项,因此您需要某种方式来同步运行它。或者如果你有一个Async.Parallel,你可能会同时发生数百个任务,但是你希望它们在继续之前完成所有任务。

据我所知,你使用Async.StartImmediate的原因是你有一些计算需要在当前线程(可能是一个UI线程)上运行而不会阻塞它。它是否使用协同程序?我想你可以称之为,虽然在.Net中没有一般的协程机制。

那么为什么Async.Parallel需要一系列Async<'T>?可能是因为它是一种组合Async<'T>对象的方式。您可以轻松创建自己的抽象,只使用普通函数(或普通函数和Async的组合,但它只是一个便利函数。

答案 6 :(得分:0)

在异步块中,您可以进行一些同步操作和一些异步操作,因此,例如,您可能有一个网站,它将以多种方式显示用户的状态,因此您可以显示他们是否有账单即将到期,生日即将到来,作业将到期。这些都不在同一个数据库中,因此您的应用程序将进行三次单独的调用。您可能希望并行进行调用,以便在最慢的调用完成后,您可以将结果放在一起并显示它,因此,最终结果将是显示基于最慢的显示。你不关心这些回来的顺序,你只想知道什么时候收到这三个。

要完成我的示例,您可能希望同步执行工作以创建用于显示此信息的UI。因此,最后,您希望获取此数据并显示UI,订单无关紧要的部分是并行完成的,订单的重要性可以同步完成。

你可以将它们作为三个线程来执行,但是当第三个线程完成时你必须跟踪和取消原始线程,但是更多的工作,让.NET框架更容易处理这个问题。