繁体   English   中英

理解F#异步编程

[英]Understanding F# Asynchronous Programming

我有点了解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() }

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

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

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

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

  let! resp = req.AsyncGetResponse()
  let stream = resp.GetResponseStream()

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

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

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

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

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

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

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

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

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

Blah1()
let! x = AsyncOp()
Blah2()

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

我认为理解异步工作流最重要的是它们的顺序与用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() )

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

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

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

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

这不是关于“获得的时间”。 异步编程不会使数据更快地到达。 相反,它是关于简化并发的心智模型。

例如,在C#中,如果要执行异步操作,则需要开始使用回调,并将本地状态传递给这些回调,依此类推。 对于像Expert F#中的两个异步操作这样的简单操作,您将看到三个看似独立的方法(启动器和两个回调)。 这掩盖了工作流的顺序概念线性特性:请求,读取流,打印结果。

相比之下,F#异步工作流程代码使程序的排序非常清晰。 只需查看一个代码块,就可以确切地知道发生了什么。 你不需要追逐回调。

也就是说,如果正在进行多个独立的异步操作,F#确实有一些机制可以帮助节省时间。 例如,您可以同时启动多个异步工作流,它们将并行运行。 但是在单个异步工作流实例中,它主要是关于简单性,安全性和可理解性:关于让你理解异步语句序列就像你推理C#式同步语句序列一样容易。

这是一个很好的问题。 请务必注意, async块中的多个语句不是并行运行的。 当异步请求处于挂起状态时, async块实质上会为其他进程提供处理器时间 因此, async块通常不会比等效的同步操作序列运行得更快,但它将允许更多的工作总体发生。 如果您希望并行运行多个语句,那么最好不要查看任务并行库。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM