MailboxProcessor - 告诉何时停止?

时间:2011-08-09 11:18:04

标签: f#

我正在玩MailboxProcessor。因此,我编写了一些可以抓取计算机上的目录和所有子目录的代理 - 然后在每个目录中打印文件:

let fileCollector =
  MailboxProcessor.Start(fun self -> 
    let rec loop() =
      async { let! file = self.Receive()
              printfn "%s" file
              return! loop() }
    loop()) 

let folderCollector = 
  MailboxProcessor.Start(fun self -> 
    let rec loop() =
      async { let! dir = self.Receive()
              do! Async.StartChild(
                    async { let! files = Directory.AsyncGetFiles dir
                            for z in files do fileCollector.Post z }) |> Async.Ignore
              return! loop() }
    loop())

let crawler =
  MailboxProcessor.Start(fun self ->
    let rec loop() =
      async { let! dir = self.Receive()
              folderCollector.Post dir
              do! Async.StartChild(
                    async { let! dirs = Directory.AsyncGetDirectories dir
                            for z in dirs do self.Post z }) |> Async.Ignore
              return! loop() }
    loop())

crawler.Post @"C:\Projects"

printfn "Done" // Message getting fired right away, due to the async stuff.

现在我如何判断folderCollectorfileCollectorcrawler何时完成,以便最终成为printfn语句,以便在爬虫成功后调用已经抓取了所有子目录并打印了所有文件?

更新 通过使用Tomas Petricek在http://tomasp.net/blog/parallel-extra-image-pipeline.aspx中展示的技术,我设法编写了以下代码:

let folders = new BlockingQueueAgent<string>(100)
let files = new BlockingQueueAgent<string>(100)

let rec folderCollector path =
  async { do! folders.AsyncAdd(path)
          do! Async.StartChild(
                  async { let! dirs = Directory.AsyncGetDirectories path
                          for z in dirs do
                            do! folderCollector z }) |> Async.Ignore }

let fileCollector =
  async { while true do
            let! dir = folders.AsyncGet()
            do! Async.StartChild(
                    async { let! fs = Directory.AsyncGetFiles dir
                            for z in fs do
                              do! files.AsyncAdd z }) |> Async.Ignore }

let rec printFiles() =
  async { let! file = files.AsyncTryGet(75)
          match file with
          | Some s -> 
            printfn "%s" s
            return! displayFiles()
          | None -> () }

let cts = new CancellationTokenSource()
Async.Start(folderCollector @"C:\Projects", cts.Token)
Async.Start(fileCollector, cts.Token)
Async.RunSynchronously(printFiles(), cancellationToken = cts.Token)

printfn "DONE!"

更新:更新:好的,所以我混淆了以下代码:

let folders = new BlockingQueueAgent<string option>(10)
let files = new BlockingQueueAgent<string option>(10)

let folderCollector path =
  async { let rec loop path = 
            async { do! folders.AsyncAdd(Some path)
                    let! dirs = Directory.AsyncGetDirectories path
                    do! [ for z in dirs -> loop z ] |> Async.Parallel |> Async.Ignore } 
          do! loop path 
          do! folders.AsyncAdd(None) }

let rec fileCollector() =
  async { let! dir = folders.AsyncGet 125
          match dir with
          | Some s -> 
            let fs = Directory.GetFiles s
            do! [ for z in fs -> printfn "%s" z; files.AsyncAdd(Some z) ] |> Async.Parallel |> Async.Ignore // <-- Fails silence if files are full
            do! fileCollector() // <-- unreachable
          | None -> printfn "Done!"; ()}

看起来很好呃?出于某种原因,在do! fileCollector()函数的fileCollector()行,不会执行 如果files BlockingQueueAgent已满。相反,它失败了。

但是,如果我这样做:

let folderCollector path =
  async { let rec loop path = 
            async { do! folders.AsyncAdd(Some path)
                    let! dirs = Directory.AsyncGetDirectories path
                    do! [ for z in dirs -> loop z ] |> Async.Parallel |> Async.Ignore } 
          do! loop path 
          do! folders.AsyncAdd(None) }

let rec fileCollector() =
  async { let! dir = folders.AsyncGet 75
          match dir with
          | Some s -> 
            let fs = Directory.GetFiles s
            do! Async.StartChild(async { do! [ for z in fs -> printfn "%s" z; files.AsyncAdd(Some z) ] 
                                             |> Async.Parallel |> Async.Ignore } ) |> Async.Ignore
            do! fileCollector()
          | None -> printfn "Done!"; ()}

它运作得很好。但是现在我无法跟踪fileCollector何时完成,因为它正在运行一堆异步计算,因此即使它在队列中达到“无”,它仍然可能还有一些工作要做。发生了什么事?


更新 我已将fileCollector修改为与folderCollector相同的“样式”,但问题仍然存在。修改后的版本:

let fileCollector() =
  async { let rec loop() = 
            async { let! dir = folders.AsyncGet 750
                    match dir with
                    | Some s -> 
                      let! fs = Directory.AsyncGetFiles s
                      do! [ for z in fs -> printfn "%A" z; files.AsyncAdd(Some z) ] 
                            |> Async.Parallel |> Async.Ignore 
                      return! loop()
                    | None -> printfn "Done!"; () }
          do! loop()
          printfn "after" // Never gets this far... 
          do! files.AsyncAdd(None) }

2 个答案:

答案 0 :(得分:3)

要回答关于基于管道的更新版本的第二个问题(来自评论) - 我认为您可以使用BlockingQueueAgent<option<string>>并在完成生成所有文件时使用值None({{然后,值将通过管道传播,您可以在获得None)时结束所有工作流。

为此,您需要修改None以实际检测何时完成迭代。它没有经过测试,但以下应该可以工作(重点是您需要等待递归调用的完成):

folderCollector

由于let rec folderCollector path = let rec loop path = async { do! folders.AsyncAdd(Some path) let! dirs = Directory.AsyncGetDirectories path do! [ for z in dirs do -> folderCollector z ] |> Async.Parallel |> Async.Ignore } async { do! loop path do! folders.AsyncAdd(None) } ,所有工作流程都可能获得None。当发生这种情况时,他们应该将AsyncGet发送给管道中的下一个工作人员。最后一个可以在收到None时终止:

None

答案 1 :(得分:2)

当F#代理完成时,没有内置支持通知您。实际上很难说。即使是空队列,代理仍未完成,因为它仍然可以从其他代理接收消息并重新开始工作。

在您的示例中,当所有三个代理的队列都为空时,工作完成。这可以使用CurrentQueueLength进行检查。这不是一个很好的解决方案,但它会起作用:

crawler.Post @"C:\Temp"
// Busy waiting until all queues are empty
while crawler.CurrentQueueLength <> 0 || folderCollector.CurrentQueueLength <> 0 ||
      fileCollector.CurrentQueueLength <> 0 do
    System.Threading.Thread.Sleep(10)
printfn "Done"

我认为更好的方法是以不同的方式构造代码 - 您实际上不需要使用代理来递归处理目录树。在您的版本中,目录(crawler代理)的行走与查找文件夹(folderCollector)中的文件并处理结果(fileCollector)并行完成,因此您实际上正在实施一个三步管道。

您可以使用async更轻松地实现管道,并使用阻塞队列来存储处理的即时结果。这article shows an example with image processing。我认为同样的方法对你也有用。检测管道处理何时结束应该更容易(在发送所有输入之后,您可以发送一条特殊消息指示完成,当消息到达管道末尾时,您已完成)。

另一种选择是使用asynchronous sequences,这可能是解决此类问题的一个好模式(但目前没有好的在线样本)。