[英]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#编写的普通代码(或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.