多线程 – 帮我理解F#线程

2019-06-12
在使用一些F#(通过MonoDevelop)时,我编写了一个例程,用一个线程列出目录中的文件:

let rec loop (path:string) = 
  Array.append
    (
        path |> Directory.GetFiles
    )
    (
        path 
        |> Directory.GetDirectories
        |> Array.map loop
        |> Array.concat
    )

然后是它的异步版本:

let rec loopPar (path:string) = 
  Array.append
    ( 
        path |> Directory.GetFiles
    )
    ( 
        let paths = path |> Directory.GetDirectories
        if paths <> [||] then
            [| for p in paths -> async { return (loopPar p) } |]
            |> Async.Parallel
            |> Async.RunSynchronously 
            |> Array.concat
        else 
            [||]
    )

在小目录上,异步版本工作正常.在较大的目录(例如数千个目录和文件)上,异步版本似乎挂起了.我错过了什么?

我知道创建数千个线程永远不会是最有效的解决方案 – 我只有8个CPU – 但我感到困惑的是,对于较大的目录,异步函数只是没有响应(即使在半小时后).然而,它并没有明显地失败,这令我感到困惑.是否有一个耗尽的线程池?

这些线程如何实际工作?

编辑:

根据this document

Mono >=2.8.x has a new threadpool that is much, much harder to deadlock. If you get a threadpool deadlock chances are that your program is trying to be deadlocked.

:d

是的,很可能你是压倒Mono线程池,这会使你的系统性能停滞不前.

如果你还记得一件事,那就是线程很贵.每个线程都需要自己的堆栈(大小为兆字节)和CPU时间片(需要上下文切换).因此,为短期任务启动自己的线程并不是一个好主意.这就是.NET有一个ThreadPool的原因.

ThreadPool是用于短任务的现有线程集合,它是异步工​​作流的F#用户.每当您运行F#异步操作时,它只是将操作委托给线程池.

问题是,当您在F#中同时生成数千个异步操作时会发生什么?一个简单的实现只会根据需要生成尽可能多的线程.但是,如果您需要1,000个线程,则意味着您需要1,000 x 4MB的堆栈空间.即使你有足够的内存用于所有堆栈,你的CPU也会不断地在不同的线程之间切换. (并在内存中分页本地堆栈.)

在IIRC中,Windows .NET实现足够智能,不会产生大量线程,只需将工作排队,直到有一些备用线程来执行操作.换句话说,它会继续添加线程,直到它有一个固定的数字,并只使用它们.但是,我不知道Mono的线程池是如何实现的.

tl; dr:这是按预期工作的.