forkIO и сопрограммы в Хаскеле
Я пытаюсь понять сопрограммы, но не совсем понимаю их назначение, учитывая наличие потоков с forkIO. Какие случаи использования требуют использования сопрограмм над потоками?
2 ответа
Из вашего вопроса немного неясно, говорите ли вы о конкретной реализации сопрограмм на Haskell (если да, пожалуйста, добавьте ссылку), или об общей концепции.
С помощью forkIO
и какая-то межпотоковая связь является одним из способов реализации сопрограмм. Преимущество состоит в том, что таким образом вы можете воспользоваться преимуществами наличия нескольких процессоров / ядер, но, на мой взгляд, есть несколько недостатков:
- Явный параллелизм
IO
поэтому все ваши вычисления должны выполняться вIO
монада. - Вы должны явно реализовать взаимодействие между потоками.
- Вы должны позаботиться о начальных нитях и, что более важно, об их утилизации и предотвращении голодания / тупиков.
- Архитектура (очевидно) многопоточная. В некоторых случаях это может быть недостатком. Например, вы можете захотеть, чтобы ваши вычисления были чистыми, детерминированными, однопоточными, но все же использовали концепцию сопрограмм.
Я также предположу, что ваш вопрос был об этомCoroutine
реализация.
Позвольте мне привести небольшой пример. Предположим, мы хотим вычислить большие факториалы, но, поскольку вычисления могут занимать слишком много времени, мы хотим, чтобы они делали паузу после каждого цикла, чтобы мы могли дать некоторую обратную связь пользователю. Кроме того, мы хотим указать, сколько циклов осталось вычислить:
import Control.Monad
import Control.Monad.Coroutine
import Control.Monad.Coroutine.SuspensionFunctors
import Control.Parallel
import Data.Functor.Identity
-- A helper function, a monadic version of 'pogoStick':
-- | Runs a suspendable 'Coroutine' to its completion.
pogoStickM :: Monad m => (s (Coroutine s m x) -> m (Coroutine s m x))
-> Coroutine s m x -> m x
pogoStickM spring c = resume c >>= either (pogoStickM spring <=< spring) return
factorial1 :: (Monad m) => Integer -> Coroutine (Yield Integer) m Integer
factorial1 = loop 1
where
loop r 0 = return r
loop r n = do
let r' = r * n
r' `par` yield n
(r' `pseq` loop r') (n - 1)
run1 :: IO ()
run1 = pogoStickM (\(Yield i c) -> print i >> return c) (factorial1 20) >>= print
Теперь, допустим, мы понимаем, что урожайность после каждого цикла слишком неэффективна. Вместо этого мы хотим, чтобы вызывающая сторона решала, сколько циклов мы должны вычислить, прежде чем снова приостановить. Чтобы достичь этого, мы просто заменим Yield
функтор с Request
:
factorial2 :: (Monad m) => Integer
-> Coroutine (Request Integer Integer) m Integer
factorial2 n = loop 1 n n
where
loop r t 0 = return r
loop r t n | t >= n = r' `par` request n >>= rec
| otherwise = rec t
where
rec t' = (r' `pseq` loop r') t' (n - 1)
r' = r * n
run2 :: IO ()
run2 = pogoStickM (\(Request i c) -> print i >> return (c (i - 5)))
(factorial2 30)
>>= print
В то время как наш run...
примеры IO
на основе, вычисления факториалов чисты, они допускают любую монаду (включая Identity
).
Тем не менее, используя параллелизм Haskell, мы выполняли чистые вычисления параллельно с кодом отчетности (перед выходом из сопрограммы мы создаем искру, которая вычисляет шаг умножения, используя par
).
И, возможно, самое главное, типы гарантируют, что сопрограммы не могут плохо себя вести. Нет никакого способа, как сопрограммы могли бы зайти в тупик - выдача или запрос обратной связи всегда связаны с соответствующим ответом (если вызывающий не решит не продолжать с сопрограммой, и в этом случае он автоматически удаляется сборщиком мусора, нет заблокированного потока),
Нет вариантов использования действительно нужных сопрограмм. Все, что вы можете сделать с сопрограммами, вы можете сделать с forkIO
+ какой-то канал связи. На самом деле, я считаю, что Go (язык, на котором параллелизм обходится очень дешево, как в Haskell) полностью избегает сопрограмм и делает все с параллельными легкими потоками ("goroutines").
Однако иногда forkIO
это перебор. Иногда вам не нужен параллелизм, вы просто хотите разложить проблему на концептуально отдельные потоки инструкций, которые уступают друг другу в определенных явно определенных точках.
Рассмотрим задачу чтения из файла и записи в другой. Вместо того, чтобы иметь монолитный вложенный цикл, более подходящим решением было бы создать сопрограмму чтения файла с записью файла. Когда вы решите позже напечатать файл на экран, вам вообще не нужно изменять сопрограмму чтения файла, вам нужно только составить его по-другому. Но обратите внимание, что эта проблема на самом деле не имеет ничего общего с параллелизмом, она связана с разделением задач и возможностью повторного использования.