10.39. Конкурентный и параллельный Haskell
GHC реализует несколько важных расширений Haskell для поддержки конкурентного и параллельного программирования. Сначала определим терминологию:
- Параллелизм означает выполнение программы Haskell на нескольких процессорах с целью повышения производительности. В идеале это должно происходить незаметно и без изменения семантики.
- Конкурентность означает реализацию программы с использованием нескольких потоков, выполняющих ввод-вывод. Хотя конкурентная программа Haskell может выполняться на параллельной машине, основной целью использования конкурентности является не повышение производительности, а простота и непосредственность написания программы. Поскольку потоки выполняют ввод-вывод, семантика программы неизбежно является недетерминированной.
GHC поддерживает как конкурентность, так и параллелизм.
10.39.1. Конкурентный Haskell
Конкурентный Haskell — это название расширения GHC для поддержки конкурентности. Оно включено по умолчанию, поэтому дополнительных флагов не требуется. Статья Concurrent Haskell по-прежнему является отличным ресурсом, как и Tackling the awkward squad.
Для программиста Concurrent Haskell не вводит новых языковых конструкций; вместо этого он выглядит просто как библиотека Control.Concurrent. Функции, экспортируемые этой библиотекой, включают:
- Создание и завершение потоков.
- Ожидание.
- Синхронизированные изменяемые переменные, называемые
MVars - Поддержка привязанных потоков; см. статью Extending the FFI with concurrency.
10.39.2. Программная транзакционная память
GHC теперь поддерживает новый способ координации деятельности потоков Concurrent Haskell, называемый Программной транзакционной памятью (STM). Статьи STM отлично представляют, что такое STM и как его использовать.
Основная библиотека, которую нужно использовать, это stm. Основные поддерживаемые функции:
- Атомные блоки.
- Транзакционные переменные.
- Операции для составления транзакций:
retry, иorElse. - Инварианты данных.
Все эти функции описаны в упомянутых статьях.
10.39.3. Параллельный Haskell
GHC включает поддержку выполнения программ Haskell параллельно на симметричных многопроцессорных системах с общей памятью (SMP). По умолчанию GHC выполняет вашу программу на одном процессоре; если вы хотите выполнить её параллельно, вы должны связать свою программу с -threaded и запустить её с опцией RTS -N ⟨x⟩; см. Использование параллелизма SMP). Система времени выполнения будет планировать выполнение потоков Haskell среди доступных потоков ОС, выполняя столько параллельных потоков, сколько вы указали с помощью опции RTS -N ⟨x⟩.
10.39.4. Аннотирование чистого кода для параллелизма
Обычные однопотоковые программы Haskell не получат выгоды от включения SMP-параллелизма: вы должны раскрыть параллелизм для компилятора. Один из способов сделать это — создать потоки с помощью Concurrent 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.
© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/8.10.2/docs/html/users_guide/parallel.html