Кооперативные потоки с нуля в 33 линии на Хаскеле

от автора

Хаскель отличает себя от большинства функциональных языков тем, что имеет глубокие культурные корни из области математики и информатики, которые дают обманчивое впечатление, что Хаскель плохо подходит для решения практических задач. Однако, чем больше вы знаете Хаскель, тем больше вы цените то, что теория часто является наиболее практическим решением многих общих проблем программирования. Этой статьёй хочется подчеркнуть эту точку зрения тем, что мы смешаем имеющиеся в наличии теоретические основы и создадим чистую пользовательскую систему потоков.

Тип

Хаскель является языком, где типы первичны, поэтому мы начнем с выбора соответствующего типа для представления потоков. Прежде всего мы должны указать на простом языке, какие мы хотим сделать потоки:

  • Потоки должны расширять существующие последовательности инструкций
  • Потоки должны поддерживать набор операций: разветвление, передача контроля и завершение
  • Потоки должны разрешать различные типы планировщиков

Теперь мы переводим эти понятия в Хаскель:

  • Когда вы слышите «несколько интерпретаторов / планировщики / бэкэнды» вы должны думать «свободный» (как в «свободном объекте»)
  • Когда вы слышите «последовательность команд» вы должны думать: «монады».
  • Когда вы хотите что-то «расширить» Вы должны думать: «трансформеры».

Объедините эти слова вместе, и вы получите правильное математическое решение: «свободный монадный трансформер».

Синтаксическое дерево

«Свободный монадный трансформер» — причудливое название для математического абстрактного синтаксического дерева, где последовательность играет важную роль. Мы обеспечиваем его набором инструкций и он строит нам синтаксическое дерево из этих инструкций.

Мы сказали, что мы хотим, чтобы наш поток мог или разветвляться, или передавать контроль, или прекращаться, так что давайте сделаем тип данных с вилками, отдачами, и завершением:

{-# LANGUAGE DeriveFunctor #-}  data ThreadF next = Fork  next next                   | Yield next                   | Done                   deriving (Functor) 

ThreadF представляет наш набор инструкций. Мы хотели добавить три новых инструкции, поэтому ThreadF имеет три конструктора, по одному для каждой команды: Fork, Yield, и Done.

Наш тип ThreadF представляет один узел в синтаксическом дереве. next поля из конструкторов представляют, куда дети узлов должны идти. Fork создает два пути исполнения, поэтому он имеет двоих детей. Done завершает текущий путь выполнения, поэтому он не имеет детей. Yield ни ветвится, ни прекращается, поэтому он имеет одного ребенка. deriving (Functor) часть просто говорит свободному монадному трансформеру, что next поля, это туда, куда дети должны пойти.

примерно то, что создаётся, когда исполняется deriving (Functor)

instance Functor ThreadF where       f `fmap` (Fork  next next) = Fork  (f next) (f next)       f `fmap` (Yield next)      = Yield (f next)       f `fmap` Done              = Done 

Теперь свободный монадный трансформер FreeT может построить синтаксическое дерево наших команд. Мы будем называть это дерево потоком (Thread):

-- из `free` пакета import Control.Monad.Trans.Free    type Thread = FreeT ThreadF 

Опытный Хаскель-программист будет читать этот код, как бы проговаривая «поток Thread является синтаксическим деревом построенным из инструкций ThreadF».

Инструкции

Теперь нам нужны примитивные инструкции. free пакет предоставляет liftF операцию, которая преобразует одну команду в синтаксическое дерево на один узел глубже:

yield :: (Monad m) => Thread m () yield = liftF (Yield ())  done :: (Monad m) => Thread m r done = liftF Done  cFork :: (Monad m) => Thread m Bool cFork = liftF (Fork False True) 

Вам не нужно полностью понимать, как это работает, за исключением того, чтобы заметить, что возвращённое значение каждой команды соответствует тому, что мы храним в ребенке поля узла:

  • yield команда сохраняет () в качестве своего ребенка, поэтому и возвращаемое значения функции равно ()
  • done команда не имеет детей, так что компилятор выводит, что она имеет полиморфное возвращаемое значение (т.е. r), это означает, что оно никогда не завершится
  • Команда cFork хранит логические значения в качестве детей, поэтому она возвращает Bool

cFork получила свое название потому, что она ведет себя как fork функция из C, это значит, возвращённое логическое значение говорит нам, на какой мы ветви находимся после разветвления. Если получаем False, то мы находимся на левой ветви и если мы получаем True, то мы находимся на правой ветви.

Мы можем объединить cFork и done заново, реализовав fork в более традиционном стиле Хаскеля, используя соглашение, что левая ветвь является «родителем» и правая ветвь является «ребёнком»:

import Control.Monad  fork :: (Monad m) => Thread m a -> Thread m () fork thread = do     child <- cFork     when child $ do         thread         done 

Приведенный выше код вызывает cFork, а потом как-бы говорит: «Если я ребенок, запустите раздвоенное действие, а затем остановитесь, в противном случае просто продолжайте как обычно».

Свободные монады

Обратите внимание как случилось нечто необычное в последнем фрагменте кода. Мы собрали из примитивных инструкций потока Thread функций cFork и done с использованием обозначений do, и мы получили новый поток Thread обратно. Это потому, что Хаскель позволяет нам использовать do нотацию любого типа, реализующего интерфейс монад (Monad) и наш свободный монадный трансформер автоматически определяет нужный instance монады для Thread. Восхитительно!

На самом деле, наш свободный монадный трансформер совсем не супер-умный. Когда мы собираем свободный монадный трансформер с использованием do нотации, все что делается, это подключаются эти примитивные синтаксические деревья в один узел глубиной (т.е. инструкции) в большее синтаксическое дерево. Последовательность двух команд:

do yield    done 

… обессахаривается просто в хранение второй команды (т.е. done) в качестве дочернего первой команды (т.е. yield).

Циклический диспетчер потоков

Теперь мы собираемся написать наш собственный планировщик потоков. Это будет наивный циклический планировщик:

-- Очередь с O(1) операциями над головой и хвостом import Data.Sequence   roundRobin :: (Monad m) => Thread m a -> m () roundRobin t = go (singleton t)  -- Начнём с единственного потока   where     go ts = case (viewl ts) of         -- Если очередь пуста: готово!         EmptyL   -> return ()          -- Очередь не пуста: Начинаем работать с первого потока         t :< ts' -> do             x <- runFreeT t  -- Запускаем эффекты этого потока             case x of                 -- Новые потоки идут в конец очереди                 Free (Fork t1 t2) -> go (t1 <| (ts' |> t2))                  -- Отдающие потоки идут в конец очереди                 Free (Yield   t') -> go (ts' |> t')                  -- Поток выполнен: Избавляем очередь от потока                 Free  Done        -> go ts'                 Pure  _           -> go ts' 

… и готово! Нет, правда, это всё! Это полная потоковая реализация.

Пользовательские потоки

Давайте попробуем нашу новую храбрую потоковую систему. Начнём с чего-то простого

mainThread :: Thread IO () mainThread = do     lift $ putStrLn "Forking thread #1"     fork thread1     lift $ putStrLn "Forking thread #1"     fork thread2  thread1 :: Thread IO () thread1 = forM_ [1..10] $ \i -> do     lift $ print i     yield  thread2 :: Thread IO () thread2 = replicateM_ 3 $ do     lift $ putStrLn "Hello"     yield 

Каждый из этих потоков имеет тип Thread IO (). Thread — это «монадный трансформер», а это означает, что он расширяет существующую монаду дополнительной функциональностью. В нашем случае мы расширяем IO монаду пользовательскими потоками, а это, в свою очередь, означает, что каждый раз, когда нам необходимо вызвать IO действие, мы используем lift, чтобы вставить это действие в поток Thread.

Когда мы вызываем функцию roundRobin, мы вытаскиваем наш Thread монадный трансформер, и наша потоковая программа коллапсирует до линейной последовательности инструкций в IO

>>> roundRobin mainThread :: IO () Forking thread #1 Forking thread #1 1 Hello 2 Hello 3 Hello 4 5 6 7 8 9 10 

Более того, наша потоковая система чистая! Мы можем расширять другие монады, не только IO, и всё равно получать потоковые эффекты! Например, мы можем построить потоковые Writer вычисления, где Writer — одна из многочисленных чистых монад (детальней о ней, см. на хабре):

import Control.Monad.Trans.Writer  logger :: Thread (Writer [String]) () logger = do     fork helper     lift $ tell ["Abort"]     yield     lift $ tell ["Fail"]  helper :: Thread (Writer [String]) () helper = do     lift $ tell ["Retry"]     yield     lift $ tell ["!"] 

В этот раз функция roundRobin производит чистое Writer действие, когда мы запускаем logger:

roundRobin logger :: Writer [String] () 

… и мы можем извлечь результаты команды-логирования чисто тоже:

execWriter (roundRobin logger) :: [String] 

Заметьте, как тип вычисляет чистое значение, список String в нашем случае. И мы всё ещё можем получить реальные потоки логированных значений:

>>> execWriter (roundRobin logger) ["Abort","Retry","Fail","!"] 

Заключение

Вы можете подумать, что я читер, что основная работа досталась библиотеке free, но всю функциональность, которую я использовал можно уместить в 12 строчек очень общего кода, годного к вторичному использованию.

data FreeF f a x = Pure a | Free (f x)  newtype FreeT f m a =     FreeT { runFreeT :: m (FreeF f a (FreeT f m a)) }  instance (Functor f, Monad m) => Monad (FreeT f m) where     return a = FreeT (return (Pure a))     FreeT m >>= f = FreeT $ m >>= \v -> case v of         Pure a -> runFreeT (f a)         Free w -> return (Free (fmap (>>= f) w))  instance MonadTrans (FreeT f) where     lift = FreeT . liftM Pure  liftF :: (Functor f, Monad m) => f r -> FreeT f m r liftF x = FreeT (return (Free (fmap return x))) 

Это частая тенденция в Хаскеле: когда мы используем теорию, мы получаем часто использованное, элегантное и мощное решение в шокирующие малом количестве кода.

Написание статьи было вдохновлено статьёй Пенг Ли и Стива Ждантевича «Методы языка для объединения потоков и событий». Основное отличие состоит в том, что методы продолжения были заменены на более простые методы свободной монады.

ссылка на оригинал статьи http://habrahabr.ru/post/195274/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *