Mono 下的 F# 任务并行性不会 "appear" 并行执行
F# task parallelism under Mono doesn't "appear" to execute in parallel
我有以下虚拟代码来测试 F# 中的 TPL。 (Mono 4.5、Xamarin Studio、四核 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
但是,如果我将线程休眠时间从 10 毫秒增加到 100 毫秒,我可以看到一点并行性。
我做错了什么?这是什么意思?我确实考虑过 CPU 在 TPL 可以在新线程上启动任务之前完成工作的可能性。但这对我来说没有意义。我可以增加内部虚拟循环 for j in 1 .. 1000 do ()
以再循环 1000 次。结果是一样的:没有并行性(thread.sleep
设置为 10 毫秒)。
另一方面,C# 中的相同代码产生了预期的结果:所有任务都以混合顺序(而不是连续顺序)将消息打印到 window
更新:
按照建议我改变了内部循环来做一些事情'actual'但是结果仍然是在单线程上执行
更新二:
我不是很理解Luaan 的评论,只是在朋友的PC 上做了测试。并且使用相同的代码,并行性正在工作(没有线程睡眠)。它看起来像与 Mono 有关的东西。但是Luaan能再解释一下我对TPL的期望吗?如果我有要并行执行并利用多核的任务 CPU,TPL 不是最佳选择吗?
更新 3:
我已经用不会被优化掉的伪代码再次尝试了@FyodorSoikin 的建议。不幸的是,工作负载仍然无法使 Mono TPL 使用多线程。目前我能让 Mono TPL 分配多个线程的唯一方法是强制现有线程休眠超过 20 毫秒。我没有足够的资格认定 Mono 是错误的,但我可以确认相同的代码(相同的基准测试工作负载)在 Mono 和 Windows 下具有不同的行为。
看起来 Sleep
被完全忽略了 - 看看 Task 2 loop
在启动下一个任务之前是如何打印的,这很愚蠢 - 如果线程等待 10 毫秒,就没有办法为此。
我认为原因可能是 OS 中的计时器分辨率。 Sleep
远非准确 - 很可能是 Mono(或 Mac OS)决定,因为他们不能可靠地让你在 10 毫秒内再次 运行,最好的选择就是现在让你 运行。这是 而不是 它在 Windows 上的工作方式 - 只要你不 Sleep(0)
,你肯定会失去控制;你想睡多久就睡多久至少。似乎在 Mono / Mac OS 上,想法是相反的 - OS 试图让你最多睡 的时间你指定的。如果你想睡的时间比定时器精度少,那就太糟糕了 - 没有睡眠。
但是即使它们没有被忽略,线程池仍然没有很大的压力来给你更多的线程。对于一行中的四个任务,您只阻塞了不到 100 毫秒 - 这还不足以让池开始创建新线程来处理请求(在 MS.NET 上,新线程只有在没有任何请求后才会被假脱机200 毫秒的空闲线程,IIRC)。您只是没有做足够的工作,不值得在后台处理新线程!
您可能忽略的一点是 Task.Factory.StartNew
实际上并没有启动任何新线程,从来没有。相反,它在默认任务调度程序上调度相关任务 - 基本上只是将其放入线程池队列中,作为要执行的任务 "at earliest convenience"。如果池中有一个空闲线程,第一个任务几乎立即开始 运行ning。第二个将 运行 当有另一个线程空闲时等。只有当线程使用是 "bad" 时(即线程是 "blocked" - 他们没有做任何 CPU 工作,但它们也不是免费的)线程池是否会生成新线程。
如果您查看该程序的 IL 输出,您会发现内部循环已被优化掉,因为它没有任何副作用,并且它的 return 值被完全忽略。
为了让它有意义,把一些不可优化的东西放在那里,并让它更重:与启动一个新任务的成本相比,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))
更新:
添加一个纯函数,比如你的 fact
,是不好的。编译器完全能够看到 fact
没有副作用,并且您适当地忽略了它的 return 值,因此,将其优化掉是非常酷的。你需要做一些编译器不知道如何优化的事情,比如上面的Debug.WriteLine
。
我有以下虚拟代码来测试 F# 中的 TPL。 (Mono 4.5、Xamarin Studio、四核 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
但是,如果我将线程休眠时间从 10 毫秒增加到 100 毫秒,我可以看到一点并行性。
我做错了什么?这是什么意思?我确实考虑过 CPU 在 TPL 可以在新线程上启动任务之前完成工作的可能性。但这对我来说没有意义。我可以增加内部虚拟循环 for j in 1 .. 1000 do ()
以再循环 1000 次。结果是一样的:没有并行性(thread.sleep
设置为 10 毫秒)。
另一方面,C# 中的相同代码产生了预期的结果:所有任务都以混合顺序(而不是连续顺序)将消息打印到 window
更新:
按照建议我改变了内部循环来做一些事情'actual'但是结果仍然是在单线程上执行
更新二:
我不是很理解Luaan 的评论,只是在朋友的PC 上做了测试。并且使用相同的代码,并行性正在工作(没有线程睡眠)。它看起来像与 Mono 有关的东西。但是Luaan能再解释一下我对TPL的期望吗?如果我有要并行执行并利用多核的任务 CPU,TPL 不是最佳选择吗?
更新 3:
我已经用不会被优化掉的伪代码再次尝试了@FyodorSoikin 的建议。不幸的是,工作负载仍然无法使 Mono TPL 使用多线程。目前我能让 Mono TPL 分配多个线程的唯一方法是强制现有线程休眠超过 20 毫秒。我没有足够的资格认定 Mono 是错误的,但我可以确认相同的代码(相同的基准测试工作负载)在 Mono 和 Windows 下具有不同的行为。
看起来 Sleep
被完全忽略了 - 看看 Task 2 loop
在启动下一个任务之前是如何打印的,这很愚蠢 - 如果线程等待 10 毫秒,就没有办法为此。
我认为原因可能是 OS 中的计时器分辨率。 Sleep
远非准确 - 很可能是 Mono(或 Mac OS)决定,因为他们不能可靠地让你在 10 毫秒内再次 运行,最好的选择就是现在让你 运行。这是 而不是 它在 Windows 上的工作方式 - 只要你不 Sleep(0)
,你肯定会失去控制;你想睡多久就睡多久至少。似乎在 Mono / Mac OS 上,想法是相反的 - OS 试图让你最多睡 的时间你指定的。如果你想睡的时间比定时器精度少,那就太糟糕了 - 没有睡眠。
但是即使它们没有被忽略,线程池仍然没有很大的压力来给你更多的线程。对于一行中的四个任务,您只阻塞了不到 100 毫秒 - 这还不足以让池开始创建新线程来处理请求(在 MS.NET 上,新线程只有在没有任何请求后才会被假脱机200 毫秒的空闲线程,IIRC)。您只是没有做足够的工作,不值得在后台处理新线程!
您可能忽略的一点是 Task.Factory.StartNew
实际上并没有启动任何新线程,从来没有。相反,它在默认任务调度程序上调度相关任务 - 基本上只是将其放入线程池队列中,作为要执行的任务 "at earliest convenience"。如果池中有一个空闲线程,第一个任务几乎立即开始 运行ning。第二个将 运行 当有另一个线程空闲时等。只有当线程使用是 "bad" 时(即线程是 "blocked" - 他们没有做任何 CPU 工作,但它们也不是免费的)线程池是否会生成新线程。
如果您查看该程序的 IL 输出,您会发现内部循环已被优化掉,因为它没有任何副作用,并且它的 return 值被完全忽略。
为了让它有意义,把一些不可优化的东西放在那里,并让它更重:与启动一个新任务的成本相比,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))
更新:
添加一个纯函数,比如你的 fact
,是不好的。编译器完全能够看到 fact
没有副作用,并且您适当地忽略了它的 return 值,因此,将其优化掉是非常酷的。你需要做一些编译器不知道如何优化的事情,比如上面的Debug.WriteLine
。