6.15.1.1. Конкурентный Haskell
Конкурентный Haskell — так называется расширение GHC для конкурентного выполнения. Оно включено по умолчанию, поэтому специальные флаги не требуются. Статья о конкурентном Haskell по-прежнему является отличным источником информации, как и Работа с непростой компанией.
Для программиста конкурентный Haskell не вводит новых языковых конструкций; вместо этого он представлен просто библиотекой Control.Concurrent. Функции, экспортируемые этой библиотекой, включают:
- Создание и уничтожение потоков.
- Приостановку выполнения.
- Синхронизированные изменяемые переменные, называемые
MVars - Поддержку привязанных потоков; см. статью Расширение FFI средствами конкурентного выполнения.
6.15.1.2. Параллельный Haskell
GHC поддерживает параллельное выполнение программ на Haskell на симметричных многопроцессорных системах с общей памятью (SMP). По умолчанию GHC выполняет программу на одном процессоре; чтобы она выполнялась параллельно, необходимо скомпоновать программу с -threaded и запустить её с параметром RTS -N ⟨x⟩; см. раздел Использование параллелизма SMP). Среда выполнения будет планировать выполняющиеся потоки Haskell между доступными потоками ОС, выполняя параллельно столько потоков, сколько указано параметром RTS -N ⟨x⟩.
6.15.1.3. Аннотирование чистого кода для параллельного выполнения
Обычные однопоточные программы на Haskell не получат преимуществ только от включения параллелизма SMP: необходимо явно указать компилятору на возможность параллельного выполнения. Один из способов сделать это — создавать потоки с помощью конкурентного Haskell (Конкурентный Haskell), однако самый простой способ извлечь параллелизм из чистого кода — использовать комбинатор par, тесно связанный с seq (и часто применяемый вместе с ним). Оба доступны в библиотеке parallel:
infixr 0 `par` infixr 1 `pseq` par :: a -> b -> b pseq :: a -> b -> b
Выражение (x `par` y) создаёт искру для вычисления x (до слабой головной нормальной формы) и возвращает y. Искры ставятся в очередь на выполнение в порядке FIFO, но не выполняются немедленно. Если среда выполнения обнаруживает свободный процессор, она может преобразовать искру в настоящий поток и запустить новый поток на свободном процессоре. Так доступный параллелизм распределяется между реальными процессорами.
Например, рассмотрим следующую параллельную версию нашего старого заклятого врага — nfib:
import Control.Parallel
nfib :: Int -> Int
nfib n | n <= 1 = 1
| otherwise = par n1 (pseq n2 (n1 + n2))
where n1 = nfib (n-1)
n2 = nfib (n-2)
Для значений n, превышающих 1, мы используем par, чтобы создать искру для вычисления nfib (n-1), а затем используем pseq, чтобы заставить родительский поток вычислить nfib (n-2), прежде чем перейти к сложению этих двух подвыражений. При таком подходе «разделяй и властвуй» мы создаём новый поток только для одной ветви вычисления (оставляя родительскому потоку вычисление другой ветви). Кроме того, необходимо использовать pseq, чтобы гарантировать, что родительский поток вычислит n2 до n1 в выражении (n1 + n2 + 1). Недостаточно просто переставить выражение в (n2 + n1 + 1), поскольку компилятор может не сгенерировать код, вычисляющий слагаемые слева направо.
Обратите внимание, что мы используем pseq, а не seq. Эти два варианта почти эквивалентны, но слегка различаются поведением во время выполнения: seq может вычислять аргументы в любом порядке, тогда как pseq обязан вычислить первый аргумент до второго, поэтому он лучше подходит для управления порядком вычисления вместе с par.
При использовании par общее практическое правило таково: созданное как искра вычисление должно понадобиться позднее, но не слишком скоро. Кроме того, оно не должно быть слишком маленьким, иначе затраты на его параллельный запуск окажутся слишком велики по сравнению с полученным выигрышем в параллелизме. На практике добиться правильного баланса непросто.
По статистике среды выполнения можно получить некоторое представление о том, насколько хорошо работает par; см. раздел Параметры RTS для управления сборщиком мусора.
Более сложные комбинаторы для выражения параллелизма доступны в модуле Control.Parallel.Strategies пакета parallel. Этот модуль расширяет возможности par, позволяя описывать более сложные шаблоны параллельных вычислений, например параллельный map.