我正在玩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.
现在我如何判断folderCollector
,fileCollector
和crawler
何时完成,以便最终成为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) }
答案 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,这可能是解决此类问题的一个好模式(但目前没有好的在线样本)。