2010-08-22 66 views
31

我想了解如何在F#中使用asynclet!。 我读过的所有文档都显得令人困惑。 使用Async.RunSynchronously运行异步块有什么意义?这是异步还是同步?看起来像是一个矛盾。F#的异步真的如何工作?

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

引用MS文档:

的代码,使用我们行!开始计算,然后线程暂停 ,直到结果可用,此时执行继续。

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

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

我想我错过了这里非常基本的东西。我想,在了解之后,所有文档和示例都将开始有意义。

+0

http://stackoverflow.com/questions/2444676/understanding-f-asynchronous-programming – 2010-09-07 17:17:58

回答

30

有几件事。

首先,

let resp = req.GetResponse() 

let! resp = req.AsyncGetReponse() 

之间的区别是,对于可能数百毫秒(一个永恒的CPU),其中web请求是“海上”时,前者使用一个线程(在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绑定并行性,您可以使用异步,但也可以使用TPL。)

+1

谢谢!为您在帖子中提到的关键词搜索,我在这里找到了 http://blogs.msdn.com/b/dsyme/archive/2009/10/19/release-notes-for-the-f-october-2009- release.aspx表示某些操作具有“自动返回上下文”,因此GUI操作更容易实现。但是,如果程序员不了解这个特性,它会变得非常混乱,因为乍一看,代码似乎被破坏了,但它仍然有效。 – marcus 2010-08-23 00:44:43

+1

是的,在普通情况下的便利性和代码/线程模型的整体透明度/透明度之间肯定存在折衷。 – Brian 2010-08-23 01:44:31

+1

只需对“StartImmediate”启用捕获“SynchronizationConnect”的声明发表评论即可。它不会隐式地启用捕获,但只是允许用户在计算表达式的部分同步部分中设置同步上下文,然后再执行诸如“Async.SwitchToContext(syncContext)'之类的操作来切换回UI线程。 – kasperhj 2015-06-17 18:07:12

12

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

请看下面的例子:

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#中异步构造的魔力并不是为了并行。因为简单的平行可以通过其他构造来实现,例如, Parallel.Net中的并行扩展。但是,异步IO更复杂,因为您看到线程放弃其执行,当IO完成时,IO需要唤醒其父线程。这是异步魔法的用处:在几行简明的代码中,您可以执行非常复杂的控制。

+0

我不认为你回答了正确的问题。 – Gabe 2010-08-22 03:51:06

+0

我想我是对的。我从他的问题开始:“为什么不是一系列普通函数要并行执行?”。所以我预测他是.Net的新手,因此需要看到异步IO的动机。 – 2010-08-22 04:05:31

+1

.NET和Parallel Framework扩展库之间有一个区别,这个问题更多的是后者。 – 2010-08-22 04:10:34

1

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

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

那么为什么Async.Parallel需要一个序列Async<'T>?可能是因为它是组成Async<'T>对象的一种方式。你可以很容易地创建自己的抽象,它只与普通函数(或简单函数和Async s的组合)一起工作,但它只是一个方便的功能。

0

在异步模块中,可以有一些同步和一些异步操作,所以,举例来说,你可能有一个网站会以几种方式显示用户的状态,所以你可以显示他们是否有很快到期的账单,生日和作业到期。这些都不在同样的数据库,所以你的应用程序会进行三次独立的调用,你可能想要并行地调用这些调用,这样当最慢的调用完成时,你可以把结果放在一起并显示出来,所以最终的结果是显示是基于最慢的,你不关心这些返回的顺序,你只是想知道什么时候收到了这三个。

为了完成我的示例,您可能想要同步执行创建UI以显示此信息的工作。因此,最后,您希望获取这些数据并显示UI,顺序无关紧要的部分是并行完成的,并且顺序问题可以以同步方式完成。

您可以将它们作为三个线程来完成,但是当第三个线程完成后,您必须保持跟踪并取消暂停原始线程,但这样做更有效,.NET框架更容易处理此问题。

5

最近我对异步模块中的函数进行了简要概述:here。也许它会有所帮助。

+0

非常有帮助,谢谢!但是我需要一些时间才能完全理解这一切! :-) – marcus 2010-08-23 00:17:02

8

这里有很多很好的答案,但我认为我对这个问题采取了不同的角度: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 

这是接受另一个函数函数(称为继续),在'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允许启动几个simulatenous DIY<'T>,请注意,它不启动新线程为了这样做,但重新使用调用线程。

没有这里的任何进一步的ADO的示例程序:

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被immidiately打印为控制台线程不会阻止启动工作流程。大约700ms后打印结果。

我希望这有趣的是,一些F#开发者

+1

很有意思,谢谢! – 2016-10-04 11:40:52

2

在许多其他的答案很详细,但作为我初学我被C#和F#之间的差异绊倒。

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

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

所以这是你想要做什么与谁应该做的事情的分离。

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