Как запустить параллельные вычисления в Haskell?

Если у меня есть функция, которая выполняет четыре очень длинных вычисления и возвращает список с результатом четырех вычислений, но где каждое вычисление не зависит от другого, как вы «распараллеливаете» это в Haskell?

Чтобы лучше объяснить мой вопрос, вот пример Clojure того, что я имею в виду:

(defn some-function [arg1 arg2 arg3 arg4]
  let [c1 (very-long-computation arg1)
       c2 (very-long-computation arg2)
       c3 (very-long-computation arg3)
       c4 (very-long-computation arg4)]
    [c1 c2 c3 c4])

Вы можете создать три дополнительных потока, например:

(defn some-function [arg1 arg2 arg3 arg4]
  let [c1 (future (very-long-computation arg1))
       c2 (future (very-long-computation arg2))
       c3 (future (very-long-computation arg3))
       c4 (very-long-computation arg4)] ; no need to wrap c4 in a future
    [@c1 @c2 @c3 c4])

Будет ли эквивалентно следующее в Haskell?

someFunction :: (a -> a -> a ->a) -> [a]
  do c1 <- rpar (very-long-computation arg1)
     c2 <- rpar (very-long-computation arg2)
     c3 <- rpar (very-long-computation arg3)
     c4 <- (very-long-computation arg4)
     rseq c1
     rseq c2
     rseq c3
     return (c1, c2, c3, c4)

Нужно ли rpar/rseq c4?

Подходит ли rpar/rseq для таких параллельных вычислений?

Что, если я не rseq, будет ли программа ждать позже, когда я попытаюсь получить доступ к возвращаемым значениям внутри возвращаемого списка?

Является ли это прозрачным или вам нужно сделать что-то вроде «deref», которое происходит в Clojure, когда вы используете «@»?


person Cedric Martin    schedule 29.04.2014    source источник
comment
Ознакомьтесь с потрясающей новой книгой Саймона Марлоу.   -  person jberryman    schedule 29.04.2014


Ответы (2)


Скорее всего, вы ищете async пакет. Например, если вы хотите участвовать в трех вычислениях и выбрать то, которое завершится первым:

someFunction :: IO a
someFunction = do
    c1 <- async $ veryLongComputation1
    c2 <- async $ veryLongComputation2
    c3 <- async $ veryLongComputation3
    (_, a) <- waitAny $ [c1, c2, c3]
    return a

Или вы можете использовать wait для определенных потоков async и обмениваться состоянием через stm. Это очень полезный пакет для такого рода вещей. Точная версия, которую вы запросили в ОП, будет выглядеть примерно так:

someFunction :: IO (a, b, c, d)
someFunction = do
    c1 <- async $ veryLongComputation1
    c2 <- async $ veryLongComputation2
    c3 <- async $ veryLongComputation3
    v4 <- veryLongComputation4
    -- wait for all the results and return them as a tuple
    wait $ (,,,) <$> c1 <*> c2 <*> c3 <*> (return v4)

Это, конечно, предполагает, что все c1, c2, c3 имеют побочные эффекты, и вас не интересуют результаты. wait и poll дают вам значения.

Я также очень рекомендую книгу «Параллельное и параллельное программирование на Haskell» Саймона Марлоу.

person cassandracomar    schedule 29.04.2014
comment
+1, но... Побочного эффекта нет, [@c1,@c2,@c3,c4] в примере Clojure на самом деле является возвращаемым значением. Функция не вернется до тех пор, пока не будет удалена ссылка на c1, c2 и c3 (то есть пока они не будут выполнены). - person Cedric Martin; 29.04.2014
comment
Да, в Haskell разветвление и проверка будущего на предмет его значения — это две отдельные операции. Пока вы этого не сделаете, у вас будет только Async a, а не a. Это сделано для того, чтобы вы могли контролировать, когда вы решите приостановить и wait в потоке, а когда вы просто poll посмотрите, закончено ли оно. - person cassandracomar; 29.04.2014
comment
@CedricMartin: я отредактировал ответ с учетом семантики, которую вы просили. - person cassandracomar; 29.04.2014

Я настоятельно рекомендую MonadPar. Я использовал стратегии раньше, но вам все еще нужно знать «волшебные» команды, чтобы все оценивать параллельно. По моему опыту, MonadPar просто работает.

Вот простой пример:

import Control.Monad.Par
import Control.Monad

foo :: [a] -> a
foo [x] = x
foo xs = 
   let len = length xs
       x1 = take len xs
       x2 = drop len xs
   in runPar $ do
     p1 <- spawnP $ foo x1
     p2 <- spawnP $ foo x2
     liftM2 (*) (get p1) $ get p2

Конечно, это требует достаточного параллелизма, чтобы действительно быть полезным. По моему опыту, накладные расходы параллелизма в Haskell довольно высоки.

person crockeea    schedule 29.04.2014
comment
ах, интересно читать, что в Haskell это довольно высоко. В Clojure для чего-то такого простого, как два умножения BigInt/по модулю, вы уже можете сэкономить несколько мс, просто создав будущее. Полагаю, мне придется провести несколько тестов, чтобы понять, когда станет иметь смысл использовать параллелизм в Haskell! В любом случае, мой вопрос заключался в том, чтобы получить общую картину того, как параллелизм работает в Haskell, +1 и большое спасибо. - person Cedric Martin; 29.04.2014
comment
Вы никогда не знаете: это может быть просто отражением медлительности Clojure в вычислении BigInts, а не низкими накладными расходами его потоков! - person crockeea; 29.04.2014