O GHC usa uma espécie de híbrido de multitarefa cooperativa e preventiva em sua implementação simultânea.
No nível Haskell, parece preventivo porque os threads não precisam render explicitamente e podem ser aparentemente interrompidos pelo tempo de execução a qualquer momento. Porém, no nível do tempo de execução, os threads "cedem" sempre que alocam memória. Como quase todos os threads do Haskell estão constantemente alocando, isso geralmente funciona muito bem.
No entanto, se um cálculo específico puder ser otimizado para código não alocado, ele poderá se tornar não cooperativo no nível de tempo de execução e, portanto, imprevisível no nível Haskell. Como o @Carl apontou, na verdade é a -fomit-yields
bandeira, que está implícita -O2
que permite que isso aconteça:
-fomit-yields
Informa ao GHC para omitir verificações de heap quando nenhuma alocação estiver sendo executada. Embora isso melhore os tamanhos binários em cerca de 5%, também significa que os threads executados em loops estreitos e sem alocação não serão impedidos em tempo hábil. Se for importante sempre poder interromper esses threads, você deve desativar essa otimização. Considere também recompilar todas as bibliotecas com essa otimização desativada, se você precisar garantir a interrupção.
Obviamente, no tempo de execução de thread único (sem -threaded
sinalizador), isso significa que um thread pode eliminar completamente todos os outros threads. Menos obviamente, a mesma coisa pode acontecer mesmo se você compilar -threaded
e usar +RTS -N
opções. O problema é que um encadeamento não cooperativo pode deixar de fora o próprio planejador de tempo de execução . Se, em algum momento, o encadeamento não cooperativo for o único agendado atualmente para execução, ele se tornará ininterrupto e o planejador nunca será executado novamente para considerar o agendamento de encadeamentos adicionais, mesmo que eles possam ser executados em outros encadeamentos O / S.
Se você está apenas tentando testar algumas coisas, altere a assinatura de fib
para fib :: Integer -> Integer
. Já que Integer
causa a alocação, tudo começará a funcionar novamente (com ou sem -threaded
).
Se você se deparar com esse problema no código real , a solução mais fácil, de longe, é a sugerida pelo @Carl: se você precisar garantir a interrupção dos encadeamentos, o código deve ser compilado -fno-omit-yields
, o que mantém as chamadas do agendador no código não alocado . Conforme a documentação, isso aumenta os tamanhos binários; Suponho que também venha com uma pequena penalidade de desempenho.
Como alternativa, se a computação já estiver dentro IO
, então explicitamente yield
no loop otimizado pode ser uma boa abordagem. Para um cálculo puro, você pode convertê-lo em E / S e yield
, embora normalmente possa encontrar uma maneira simples de introduzir uma alocação novamente. Na maioria das situações realistas, haverá uma maneira de introduzir apenas alguns yield
ou alocações - o suficiente para tornar o thread responsivo novamente, mas não o suficiente para afetar seriamente o desempenho. (Por exemplo, se você tiver alguns loops recursivos aninhados yield
ou forçar uma alocação no loop mais externo.)
MVar
afirmam que é suscetível a condições de corrida. Eu levaria essa nota a sério.