Haskell 计算密集型线程阻塞所有其他线程
Haskell computationally intensive thread blocks all other threads
我想编写一个程序,其主线程派生一个新线程进行计算并等待它完成一段时间。如果子线程没有在给定的时间内完成,它就会超时并被杀死。为此,我有以下代码。
import Control.Concurrent
fibs :: Int -> Int
fibs 0 = 0
fibs 1 = 1
fibs n = fibs (n-1) + fibs (n-2)
main = do
mvar <- newEmptyMVar
tid <- forkIO $ do
threadDelay (1 * 1000 * 1000)
putMVar mvar Nothing
tid' <- forkIO $ do
if fibs 1234 == 100
then putStrLn "Incorrect answer" >> putMVar mvar (Just False)
else putStrLn "Maybe correct answer" >> putMVar mvar (Just True)
putStrLn "Waiting for result or timeout"
result <- takeMVar mvar
killThread tid
killThread tid'
我用 ghc -O2 Test.hs
和 ghc -O2 -threaded Test.hs
和 运行 编译了上面的程序,但在这两种情况下,程序只是挂起而没有打印任何内容或退出。如果我在 if
块之前将 threadDelay (2 * 1000 * 1000)
添加到计算线程,那么程序将按预期工作并在一秒后完成,因为计时器线程能够填充 mvar
。
为什么线程没有像我预期的那样工作?
GHC 在其并发实现中使用了一种协作和抢占式多任务处理的混合体。
在 Haskell 级别,它似乎是先发制人的,因为线程不需要显式让步并且可以随时被 运行 时间打断。但是在 运行 时间级别,线程 "yield" 每当它们分配内存时。由于几乎所有 Haskell 线程都在不断分配,这通常工作得很好。
但是,如果可以将特定计算优化为非分配代码,则它可能在 运行 时间级别上变得不合作,因此在 Haskell 级别上不可抢占。正如@Carl 指出的那样,它实际上是 -fomit-yields
标志,由 -O2
暗示允许这种情况发生:
-fomit-yields
Tells GHC to omit heap checks when no allocation is being performed. While this improves binary sizes by about 5%, it also means that threads run in tight non-allocating loops will not get preempted in a timely fashion. If it is important to always be able to interrupt such threads, you should turn this optimization off. Consider also recompiling all libraries with this optimization turned off, if you need to guarantee interruptibility.
显然,在单线程运行时间(没有-threaded
标志),这意味着一个线程可以完全饿死所有其他线程。不太明显的是,即使您使用 -threaded
编译并使用 +RTS -N
选项,同样的事情也会发生。问题是不合作的线程可能会耗尽 运行time scheduler 本身。如果在某个时候不合作的线程是当前调度到 运行 的唯一线程,它将变得不可中断,调度程序将永远不会重新运行 考虑调度其他线程,即使它们可以 运行 在其他 O/S 个线程上。
如果您只是想测试一些东西,请将 fib
的签名更改为 fib :: Integer -> Integer
。由于 Integer
导致分配,一切都会重新开始工作(有或没有 -threaded
)。
如果您 运行 在 真实 代码中遇到这个问题,到目前为止,最简单的解决方案是 @Carl 建议的解决方案:如果您需要保证可中断性对于线程,代码应该使用 -fno-omit-yields
进行编译,这会在非分配代码中保留调度程序调用。根据文档,这会增加二进制大小;我认为它也会带来小的性能损失。
或者,如果计算已经在 IO
中,那么在优化循环中显式 yield
ing 可能是一个很好的方法。对于纯计算,您可以将其转换为 IO 和 yield
,但通常您可以找到一种简单的方法再次引入分配。在大多数现实情况下,将有一种方法只引入 "few" yield
或分配——足以使线程再次响应但不足以严重影响性能。 (例如,如果您有一些嵌套的递归循环,yield
或强制在最外层循环中进行分配。)
我想编写一个程序,其主线程派生一个新线程进行计算并等待它完成一段时间。如果子线程没有在给定的时间内完成,它就会超时并被杀死。为此,我有以下代码。
import Control.Concurrent
fibs :: Int -> Int
fibs 0 = 0
fibs 1 = 1
fibs n = fibs (n-1) + fibs (n-2)
main = do
mvar <- newEmptyMVar
tid <- forkIO $ do
threadDelay (1 * 1000 * 1000)
putMVar mvar Nothing
tid' <- forkIO $ do
if fibs 1234 == 100
then putStrLn "Incorrect answer" >> putMVar mvar (Just False)
else putStrLn "Maybe correct answer" >> putMVar mvar (Just True)
putStrLn "Waiting for result or timeout"
result <- takeMVar mvar
killThread tid
killThread tid'
我用 ghc -O2 Test.hs
和 ghc -O2 -threaded Test.hs
和 运行 编译了上面的程序,但在这两种情况下,程序只是挂起而没有打印任何内容或退出。如果我在 if
块之前将 threadDelay (2 * 1000 * 1000)
添加到计算线程,那么程序将按预期工作并在一秒后完成,因为计时器线程能够填充 mvar
。
为什么线程没有像我预期的那样工作?
GHC 在其并发实现中使用了一种协作和抢占式多任务处理的混合体。
在 Haskell 级别,它似乎是先发制人的,因为线程不需要显式让步并且可以随时被 运行 时间打断。但是在 运行 时间级别,线程 "yield" 每当它们分配内存时。由于几乎所有 Haskell 线程都在不断分配,这通常工作得很好。
但是,如果可以将特定计算优化为非分配代码,则它可能在 运行 时间级别上变得不合作,因此在 Haskell 级别上不可抢占。正如@Carl 指出的那样,它实际上是 -fomit-yields
标志,由 -O2
暗示允许这种情况发生:
-fomit-yields
Tells GHC to omit heap checks when no allocation is being performed. While this improves binary sizes by about 5%, it also means that threads run in tight non-allocating loops will not get preempted in a timely fashion. If it is important to always be able to interrupt such threads, you should turn this optimization off. Consider also recompiling all libraries with this optimization turned off, if you need to guarantee interruptibility.
显然,在单线程运行时间(没有-threaded
标志),这意味着一个线程可以完全饿死所有其他线程。不太明显的是,即使您使用 -threaded
编译并使用 +RTS -N
选项,同样的事情也会发生。问题是不合作的线程可能会耗尽 运行time scheduler 本身。如果在某个时候不合作的线程是当前调度到 运行 的唯一线程,它将变得不可中断,调度程序将永远不会重新运行 考虑调度其他线程,即使它们可以 运行 在其他 O/S 个线程上。
如果您只是想测试一些东西,请将 fib
的签名更改为 fib :: Integer -> Integer
。由于 Integer
导致分配,一切都会重新开始工作(有或没有 -threaded
)。
如果您 运行 在 真实 代码中遇到这个问题,到目前为止,最简单的解决方案是 @Carl 建议的解决方案:如果您需要保证可中断性对于线程,代码应该使用 -fno-omit-yields
进行编译,这会在非分配代码中保留调度程序调用。根据文档,这会增加二进制大小;我认为它也会带来小的性能损失。
或者,如果计算已经在 IO
中,那么在优化循环中显式 yield
ing 可能是一个很好的方法。对于纯计算,您可以将其转换为 IO 和 yield
,但通常您可以找到一种简单的方法再次引入分配。在大多数现实情况下,将有一种方法只引入 "few" yield
或分配——足以使线程再次响应但不足以严重影响性能。 (例如,如果您有一些嵌套的递归循环,yield
或强制在最外层循环中进行分配。)