理解F#异步编程

Yin*_*Zhu 22 f# asynchronous

我有点了解F#中异步编程的语法.例如

let downloadUrl(url:string) = async { 
  let req = HttpWebRequest.Create(url)
  // Run operation asynchronously
  let! resp = req.AsyncGetResponse()
  let stream = resp.GetResponseStream()
  // Dispose 'StreamReader' when completed
  use reader = new StreamReader(stream)
  // Run asynchronously and then return the result
  return! reader.AsyncReadToEnd() }
Run Code Online (Sandbox Code Playgroud)

在F#专家书(和许多其他来源),他们说喜欢

让!var = expr只是表示"执行异步操作expr并在操作完成时将结果绑定到var.然后继续执行其余的计算主体"

我也知道在执行异步操作时会创建一个新线程.我最初的理解是异步操作后有两个并行线程,一个执行I/O,另一个继续同时执行异步主体.

但在这个例子中,我很困惑

  let! resp = req.AsyncGetResponse()
  let stream = resp.GetResponseStream()
Run Code Online (Sandbox Code Playgroud)

如果resp尚未启动并且异步体中的线程想要发生GetResponseStream什么?这可能是错误吗?

也许我原来的理解是错误的.F#专家书中引用的句子实际上意味着"创建一个新线程,挂起当前线程,当新线程完成时,唤醒正文线程并继续",但在这种情况下我看不到我们可以保存随时.

在最初的理解中,当一个异步块中有多个独立的 IO操作时,可以节省时间,这样它们可以在不相互干预的情况下同时完成.但是在这里,如果我没有得到回复,我就无法创建流; 只有我有流,我可以开始阅读流.获得的时间在哪里?

Bri*_*ian 23

此示例中的"异步"不是关于并发或节省时间,而是关于提供良好的编程模型而不阻塞(读取:浪费)线程.

如果使用其他编程语言,通常有两种选择:

您可以阻止,通常通过调用同步方法.缺点是线程在等待磁盘或网络I/O或您拥有的东西时被占用并且没有做任何有用的工作.优点是代码简单(普通代码).

您可以使用回调来异步调用,并在操作完成时收到通知.优点是您不会阻塞线程(这些线程可以返回到例如ThreadPool,并且当操作完成后将使用新的ThreadPool线程来回调).缺点是一个简单的代码块被分成一堆回调方法或lambda,并且很快就会在回调中维护状态/控制流/异常处理变得非常复杂.

所以你在岩石和坚硬的地方之间; 你要么放弃简单的编程模型,要么浪费线程.

F#模型提供了两全其美的效果; 你不会阻止线程,但你保持简单的编程模型.像这样的构造let!使您能够在异步块的中间"跳线",所以在代码中

Blah1()
let! x = AsyncOp()
Blah2()
Run Code Online (Sandbox Code Playgroud)

Blah1例如,ThreadPool线程#13可以运行,但随后AsyncOp会将该线程释放回ThreadPool.稍后当AsyncOp完成时,其余代码将在可用线程(可能是ThreadPool线程#20)上开始备份,该线程绑定x到结果然后运行Blah2.在简单的客户端应用程序中,这很少重要(除非确保您不阻止UI线程),但在执行I/O的服务器应用程序中(线程通常是宝贵的资源 - 线程很昂贵,您不能浪费它们阻塞)非阻塞I/O通常是使应用程序扩展的唯一方法.F#使您能够编写非阻塞I/O,而无需将程序降级为大量的意大利面条代码回调.

也可以看看

使用异步工作流并行化的最佳实践

如何在F#中进行链式回调?

http://cs.hubfs.net/forums/thread/8262.aspx


Tom*_*cek 9

我认为理解异步工作流最重要的是它们的顺序与用F#编写的普通代码(或C#,就此而言)是顺序的相同.你有一些let以通常顺序和一些表达式(可能有副作用)评估的绑定.实际上,异步工作流通常看起来更像命令式代码.

异步工作流的第二个重要方面是它们是非阻塞的.这意味着您可以执行以某种非标准方式执行的操作,并且在执行时不会阻塞该线程.(通常,let!在F#计算表达式中始终表示存在一些非标准行为 - 如果不在Maybe monad中生成结果,则可能会失败,或者它可能是异步工作流的非阻塞执行).

从技术上讲,非阻塞执行是通过注册一些将在操作完成时触发的回调来实现的.相对简单的示例是等待某个指定时间的异步工作流 - 这可以使用Timer而不阻塞任何线程来实现(示例来自我的书的第13章,源代码可在此处获得):

// Primitive that delays the workflow
let Sleep(time) = 
  // 'FromContinuations' is the basic primitive for creating workflows
  Async.FromContinuations(fun (cont, econt, ccont) ->
    // This code is called when workflow (this operation) is executed
    let tmr = new System.Timers.Timer(time, AutoReset=false)
    tmr.Elapsed.Add(fun _ -> 
      // Run the rest of the computation
      cont())
    tmr.Start() )
Run Code Online (Sandbox Code Playgroud)

还有几种方法可以将F#异步工作流用于并行或并发编程,但这些只是F#工作流或基于它们构建的库的更复杂用法 - 它们利用了前面描述的非阻塞行为.

  • 您可以使用StartChild在后台启动工作流 - 该方法为您提供了一个正在运行的工作流,您可以let!在工作流中稍后使用(使用)等待完成,同时您可以继续执行其他操作.这类似于.NET 4.0中的任务,但它以异步方式运行,因此更适合I/O操作.

  • 您可以使用Async.Parallel创建多个工作流并等待所有工作流完成(这对于数据并行操作非常有用).这类似于PLINQ,但是再次,async如果你做一些I/O操作会更好.

  • 最后,您可以使用MailboxProcessor允许您使用消息传递样式(Erlang样式)编写并发应用程序.对于许多问题,这是线程的一个很好的替代方案.

  • 我是`MailboxProcessor`的忠实粉丝.起初它可能有点棘手,但是一旦掌握了它,你就可以编写出非常优雅的代码. (2认同)