好问题!然而,自从如何在 GHCI 中中止执行? https://stackoverflow.com/questions/30877019/how-to-abort-execution-in-ghci已经重点介绍了第二部分,我们在这里不再重复。相反,让我们关注第一点。
为什么会这样呢?
GHC 积极优化循环。如果没有分配,它会进一步优化它们这甚至是一个已知的错误 https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/bugs.html#bugs-in-ghc:
19.2.1。 GHC 中的错误
-
GHC 的运行时系统实现了协作式多任务处理,只有在程序分配时才可能发生上下文切换。这意味着不分配的程序可能永远不会进行上下文切换。对于使用 STM 的程序尤其如此,在观察到不一致的状态后可能会出现死锁。 See 追踪 #367 https://ghc.haskell.org/trac/ghc/ticket/367以供进一步讨论。 [强调我的]
如果您遇到此问题,您可能需要使用以下命令编译受影响的模块-fno-omit-yields https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/using-optimisation.html#ghc-flag--fomit-yields (see -f*:与平台无关的标志 https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/using-optimisation.html#options-f)。该标志确保在每个函数入口点插入让出点(以牺牲一点性能为代价)。
如果我们检查-fomit-yields https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/using-optimisation.html#ghc-flag--fomit-yields, 我们发现:
-fomit-yields
默认值:启用屈服点
告诉 GHC 在未执行分配时忽略堆检查。虽然这将二进制大小提高了约 5%,但这也意味着
在紧密的非分配循环中运行的线程不会被抢占
及时。如果总是能够打断很重要
对于此类线程,您应该关闭此优化。还请考虑
重新编译所有库并关闭此优化,如果您
需要保证可中断性。[强调我的]
nub $ cycle "ab"
是一个紧密的、非分配的循环,尽管last $ repeat 1
是一个更明显的非分配示例。
“启用屈服点”具有误导性:-fomit-yields
默认启用。由于标准库是用-fomit-yields
, 标准库中所有导致紧密、非分配循环的函数可能会在 GHCi 中表现出这种行为,因为您永远不会重新编译它们。
我们可以通过以下程序验证这一点:
-- Test.hs
myLast :: [a] -> Maybe a
myLast [x] = Just x
myLast (_:xs) = myLast xs
myLast _ = Nothing
main = print $ myLast $ repeat 1
We can use C-c to quit it if we run it in GHCi without compiling beforehand:
$ ghci Test.hs
[1 of 1] Compiling Main ( Test.hs, interpreted )
Ok, one module loaded.
*Main> :main <pressing C-c after a while>
Interrupted.
如果我们编译它然后在 GHCi 中重新运行它,它将挂起:
$ ghc Test.hs
[1 of 1] Compiling Main ( Test.hs, Test.o )
Linking Test.exe ...
$ ghci Test.hs
Ok, one module loaded.
*Main> :main
<hangs indefinitely>
请注意,您需要-dynamic
如果您不使用Windows,否则GHCi将重新编译源文件。但是,如果我们使用-fno-omit-yield
,我们突然可以再次退出(在 Windows 中)。
我们可以用另一个小片段再次验证这一点:
Prelude> last xs = case xs of [x] -> x ; (_:ys) -> last ys
Prelude> last $ repeat 1
^CInterrupted
As ghci
不使用任何优化,它也不使用-fomit-yield
(因此有-fno-omit-yield
已启用)。我们的新变种last
不会产生与以下相同的行为Prelude.last
因为它不是用编译的fomit-yield
.
现在我们知道了why发生这种情况时,我们知道我们将在整个标准库中经历这种行为,因为标准库是用-fomit-yield
.