Mono下的F#任务并行不会"出现"并行执行

cas*_*bby 4 mono f# asynchronous task-parallel-library

我有以下虚拟代码来测试F#中的TPL.(Mono 4.5,Xamarin工作室,四核MacBook Pro)

令我惊讶的是,所有进程都在同一个线程上完成.根本就没有并行性.

open System
open System.Threading
open System.Threading.Tasks


let doWork (num:int) (taskId:int) : unit =
    for i in 1 .. num do
        Thread.Sleep(10)
        for j in 1 .. 1000 do
            ()
        Console.WriteLine(String.Format("Task {0} loop: {1}, thread id {2}", taskId, i, Thread.CurrentThread.ManagedThreadId)) 

[<EntryPoint>]
let main argv = 

    let t2 = Task.Factory.StartNew(fun() -> doWork 10 2)
    //printfn "launched t2"
    Console.WriteLine("launched t2")
    let t1 = Task.Factory.StartNew(fun() -> doWork 8 1)
    Console.WriteLine("launched t1")
    let t3 = Task.Factory.StartNew(fun() -> doWork 10 3)
    Console.WriteLine("launched t3")
    let t4 = Task.Factory.StartNew(fun() -> doWork 5 4)
    Console.WriteLine("launched t4")
    Task.WaitAll(t1,t2,t3,t4)
    0 // return an integer exit code
Run Code Online (Sandbox Code Playgroud)

但是,如果我将线程休眠时间从10毫秒增加到100毫秒,我可以看到一点并行性.

我做错了什么?这是什么意思?在TPL可以在新线程上启动任务之前,我确实考虑过CPU完成工作的可能性.但这对我来说没有意义.我可以增加内部虚拟循环for j in 1 .. 1000 do ()以循环1000次.结果是相同的:没有并行性(thread.sleep设置为10毫秒).

另一方面,C#中的相同代码会产生所需的结果:所有任务以混合顺序(而不是顺序顺序)将消息打印到窗口

更新:

正如所建议的,我改变了内部循环来做一些"实际"的事情,但结果仍然是在单个线程上执行

更新2:

我不太了解Luaan的评论,但我刚刚在朋友的PC上做过测试.使用相同的代码,并行性正在工作(没有线程休眠).它看起来与Mono有关.但Luaan可以再解释一下我对TPL的期望吗?如果我有要并行执行的任务并利用多核CPU,那么TPL不是最佳选择吗?

更新3:

我已经用虚拟代码再次尝试了@ FyodorSoikin的建议,这些代码不会被优化掉.不幸的是,工作负载仍然无法使Mono TPL使用多个线程.目前,我可以让Mono TPL分配多个线程的唯一方法是强制在现有线程上休眠超过20ms.我没有资格证明Mono是错误的,但我可以确认相同的代码(相同的基准工作负载)在Mono和Windows下具有不同的行为.

Lua*_*aan 6

它看起来像Sleeps被完全忽略 - 看看Task 2 loop在启动下一个任务之前如何打印,这只是愚蠢 - 如果线程等待10ms,就没有办法实现.

我假设原因可能是操作系统中的计时器分辨率.这Sleep远非准确 - 很可能Mono(或Mac OS)决定由于它们无法让你在10ms内再次运行,最好的选择就是让你现在就跑.这不是它在Windows上的工作方式 - 只要你不这样做,你就可以保证失控Sleep(0); 你总是会你想要的时候睡觉.似乎在Mono/Mac OS上,这个想法是相反的 - 操作系统试图让你最多睡觉你指定的时间.如果你想睡觉的时间少于计时器的精确度,那就太糟糕了 - 没有睡觉.

但即使它们没有被忽略,线程池上仍然没有很大的压力来为你提供更多线程.对于一行中的四个任务,你只能阻塞不到100毫秒 - 这对于池开始创建新线程来处理请求还不够(在MS.NET上,新线程只在没有任何空闲线程后假脱机200分钟,IIRC).你只是没有做足够的工作,因此值得花一些新线程!

您可能缺少的一点是,Task.Factory.StartNew实际上并没有开始任何新线程.相反,它正在调度默认任务调度程序上的相关任务 - 它只是将它放在线程池队列中,作为"尽早"执行的任务,基本上.如果池中有一个空闲线程,则第一个任务几乎立即开始在那里运行.第二个将在另一个线程空闲时运行等.只有当线程使用"坏"时(即线程被"阻塞" - 它们没有做任何CPU工作,但它们也不是免费的)是线程池正在运行产生新线程.


Fyo*_*kin 5

如果你看看这个程序的IL输出,你会看到内部循环被优化掉了,因为它没有任何副作用,并且它的返回值被完全忽略.

为了计算数量,在那里放置一些不可优化的东西,并使其更重:与开启新任务的成本相比,1000个空循环几乎不可察觉.

例如:

let doWork (num:int) (taskId:int) : unit =
    for i in 1 .. num do
        Thread.Sleep(10)
        for j in 1 .. 1000 do
            Debug.WriteLine("x")
        Console.WriteLine(String.Format("Task {0} loop: {1}, thread id {2}", taskId, i, Thread.CurrentThread.ManagedThreadId)) 
Run Code Online (Sandbox Code Playgroud)

更新:
添加一个纯函数,如你的fact,是不好的.编译器完全能够看到它fact没有副作用,并且你正确地忽略它的返回值,因此,优化它是非常酷的.您需要执行编译器不知道如何优化的操作,例如Debug.WriteLine上面的内容.