为什么要在异步中嵌入异步?

Why embed async in async?

我从 Expert f# 书中阅读了以下代码,

  1. 为什么函数 collectLinkslet! html = async { .... } 嵌入到外部 async 块中?通过删除内部异步来平整它怎么样?

  2. urlCollector 中函数 waitForUrl 的相同问题,它在外部 async 块中有一个 do! Async.StartChild (async {....}) |> Async.Ignore。扁平化怎么样?

  3. 这个实现和用block queue实现的相比怎么样? https://msdn.microsoft.com/en-us/library/vstudio/hh297096(v=vs.100).aspx 创建一个包含 5 的块队列,并将 link 加入生产者队列。

代码:

open System.Collections.Generic
open System.Net
open System.IO
open System.Threading
open System.Text.RegularExpressions

let limit = 50
let linkPat = "href=\s*\"[^\"h]*(http://[^&\"]*)\""
let getLinks (txt:string) =
    [ for m in Regex.Matches(txt,linkPat)  -> m.Groups.Item(1).Value ]

// A type that helps limit the number of active web requests
type RequestGate(n:int) =
    let semaphore = new Semaphore(initialCount=n, maximumCount=n)
    member x.AsyncAcquire(?timeout) =
        async { let! ok = Async.AwaitWaitHandle(semaphore,
                                                ?millisecondsTimeout=timeout)
                if ok then
                   return
                     { new System.IDisposable with
                         member x.Dispose() =
                             semaphore.Release() |> ignore }
                else
                   return! failwith "couldn't acquire a semaphore" }

// Gate the number of active web requests
let webRequestGate = RequestGate(5)

// Fetch the URL, and post the results to the urlCollector.
let collectLinks (url:string) =
    async { // An Async web request with a global gate
            let! html =
                async { // Acquire an entry in the webRequestGate. Release
                        // it when 'holder' goes out of scope
                        use! holder = webRequestGate.AsyncAcquire()

                        let req = WebRequest.Create(url,Timeout=5)

                        // Wait for the WebResponse
                        use! response = req.AsyncGetResponse()

                        // Get the response stream
                        use reader = new StreamReader(response.GetResponseStream())

                        // Read the response stream (note: a synchronous read)
                        return reader.ReadToEnd()  }

            // Compute the links, synchronously
            let links = getLinks html

            // Report, synchronously
            do printfn "finished reading %s, got %d links" url (List.length links)

            // We're done
            return links }

/// 'urlCollector' is a single agent that receives URLs as messages. It creates new
/// asynchronous tasks that post messages back to this object.
let urlCollector =
    MailboxProcessor.Start(fun self ->

        // This is the main state of the urlCollector
        let rec waitForUrl (visited : Set<string>) =

           async { // Check the limit
                   if visited.Count < limit then

                       // Wait for a URL...
                       let! url = self.Receive()
                       if not (visited.Contains(url)) then
                           // Start off a new task for the new url. Each collects
                           // links and posts them back to the urlCollector.
                           do! Async.StartChild
                                   (async { let! links = collectLinks url
                                            for link in links do
                                               self.Post link }) |> Async.Ignore

                       // Recurse into the waiting state
                       return! waitForUrl(visited.Add(url)) }

        // This is the initial state.
        waitForUrl(Set.empty))

我能想到为什么 async 代码会调用另一个异步块的一个原因,那就是它可以让您更早地处理资源 - 当嵌套块完成时。为了演示这一点,这里有一个小助手,它在调用 Dispose 时打印一条消息:

let printOnDispose text = 
  { new System.IDisposable with
      member x.Dispose() = printfn "%s" text }

下面使用nested async在嵌套块中做一些事情,然后清理嵌套块中使用的本地资源。然后它再睡一会儿并清理外部块中使用的资源:

async { 
  use bye = printOnDispose "bye from outer block"
  let! r = async {
    use bye = printOnDispose "bye from nested block"
    do! Async.Sleep(1000)
    return 1 }
  do! Async.Sleep(1000) }
|> Async.Start

这里"nested block"资源1秒后处理,外块资源2秒后处理

在其他情况下嵌套 async 很有用(例如从包含 try .. with 的异步块返回),但我认为这不适用于此处。