【问题标题】:Understanding F# Asynchronous Programming了解 F# 异步编程
【发布时间】:2010-03-15 21:57:49
【问题描述】:

我有点了解 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操作可以同时进行而不相互干预,这样可以节省时间。但是在这里,如果我没有得到响应,我就无法创建流;只有我有流,我可以开始阅读流。哪来的时间?

【问题讨论】:

标签: f# asynchronous


【解决方案1】:

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

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

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

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

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

F# 模型两全其美;你不会阻塞线程,但你保持简单的编程模型。 let! 之类的构造使您能够在异步块的中间“线程跳跃”,因此在代码中类似于

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

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

另见

Best practices to parallelize using async workflow

How to do chained callbacks in F#?

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

【讨论】:

    【解决方案2】:

    我认为了解异步工作流最重要的一点是,它们顺序与用 F#(或 C#,就此而言)编写的普通代码相同) 是顺序的。您有一些let 绑定,它们以通常的顺序进行计算,还有一些表达式(可能有副作用)。事实上,异步工作流通常看起来更像命令式代码。

    异步工作流的第二个重要方面是它们非阻塞。这意味着您可以拥有以某种非标准方式执行的操作,并且在执行时不会阻塞线程。 (一般来说,F# 计算表达式中的let! 总是表示存在一些非标准 行为——它可能会失败而不会在Maybe monad 中产生结果,或者它可能是非阻塞异步工作流的执行)。

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

    // 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 中的 Tasks,但它是异步运行的,因此更适合 I/O 操作。

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

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

    【讨论】:

    • 我是MailboxProcessor 的忠实粉丝。一开始可能会有点棘手,但一旦掌握了窍门,您就可以编写出非常优雅的代码。
    【解决方案3】:

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

    例如,在 C# 中,如果要执行异步操作,则需要开始处理回调,并将本地状态传递给这些回调,等等。对于像 Expert F# 中具有两个异步操作的简单操作,您正在查看三个看似独立的方法(启动器和两个回调)。这掩盖了工作流程的顺序、概念上线性的性质:执行请求、读取流、打印结果。

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

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

    【讨论】:

      【解决方案4】:

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

      【讨论】:

        猜你喜欢
        • 2016-09-28
        • 1970-01-01
        • 2012-10-07
        • 1970-01-01
        • 2012-02-05
        • 2015-01-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多