GH C использует своего рода гибрид кооперативной и вытесняющей многозадачности в своей реализации параллелизма.
На уровне Haskell это кажется вытесняющим, потому что потоки не должны явно выдавать и могут быть казалось бы, прервано во время выполнения в любое время. Но на уровне времени выполнения потоки «уступают» всякий раз, когда выделяют память. Поскольку почти все потоки Haskell постоянно выделяются, это обычно работает довольно хорошо.
Однако, если конкретный расчет может быть оптимизирован для нераспределенного кода, он может перестать работать на уровне времени выполнения и, таким образом, выкупаем на уровне Haskell. Как указал @Carl, на самом деле это флаг -fomit-yields
, который подразумевается -O2
, который позволяет этому произойти:
-fomit-yields
Говорит GH C, чтобы пропустить проверку кучи, когда распределение не выполняется. Хотя это улучшает размер двоичных файлов примерно на 5%, это также означает, что потоки, работающие в узких невыделенных циклах, не будут своевременно вытеснены. Если важно всегда иметь возможность прерывать такие потоки, вы должны отключить эту оптимизацию. Рассмотрим также перекомпиляцию всех библиотек с отключенной оптимизацией, если вам нужно гарантировать прерывание.
Очевидно, что в однопоточном времени выполнения (без флага -threaded
) это означает, что один поток может полностью истощить все остальные темы. Менее очевидно, то же самое может произойти, даже если вы компилируете с -threaded
и используете опции +RTS -N
. Проблема в том, что неработающий поток может истощить саму среду выполнения scheduler . Если в какой-то момент неработающий поток является единственным потоком, запланированным на текущий момент для выполнения, он станет бесперебойным, и планировщик никогда не будет перезапущен для рассмотрения планирования дополнительных потоков, даже если они могут выполняться в других потоках O / S.
Если вы просто пытаетесь что-то протестировать, измените подпись fib
на fib :: Integer -> Integer
. Поскольку Integer
вызывает распределение, все снова начнет работать (с -threaded
или без него).
Если вы столкнетесь с этой проблемой в реальном коде, самое простое решение, безусловно, @Carl предлагает тот, что: если вам нужно гарантировать прерывание потоков, код должен быть скомпилирован с -fno-omit-yields
, который сохраняет вызовы планировщика в нераспределенном коде. Согласно документации это увеличивает двоичные размеры; Я предполагаю, что это также приводит к небольшому снижению производительности.
В качестве альтернативы, если вычисление уже находится в IO
, тогда явно yield
в оптимизированном l oop может будь хорошим подходом. Для чистого вычисления вы можете преобразовать его в IO и yield
, хотя обычно вы можете найти простой способ снова ввести распределение. В большинстве реальных c ситуаций будет способ ввести только «несколько» yield
с или выделений - достаточно, чтобы поток снова стал отзывчивым, но недостаточно, чтобы серьезно повлиять на производительность. (Например, если у вас есть несколько вложенных рекурсивных циклов, yield
или принудительное выделение в самом внешнем l oop.)