"Мода-дружественный" основанный на событиях IO

Я хочу реализовать эффективную однопоточную связь сокетов с помощью управления событиями epoll ".

Если бы я написал очень императивную программу "с нуля", я бы сделал это в основном так (только какой-то псевдо-код, который я только что напечатал, вероятно, не будет компилироваться):

import Control.Concurrent

import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString

import qualified GHC.Event as Event

import Network
import Network.Socket
import Network.Socket.ByteString

main = withSocketFromSomewhere $ \ socket -> do
  let fd = fromIntegral . fdSocket $ socket

  -- Some app logic
  state <- newMVar "Bla"

  -- Event manager
  manager <- Event.new

  -- Do an initial write
  initialWrite socket state manager

  -- Manager does its thing
  Event.loop manager

write manager socket bs =
  -- Should be pretty straight-forward
  Event.registerFd manager theWrite fd Event.evtWrite
  where
    fd = fromIntegral . fdSocket $ socket
    theWrite key _ = do
      Event.unregisterFd manager key
      sendAll socket bs

read manager socket cont =
  -- Ditto
  Event.registerFd manager theRead fd Event.evtRead
  where
    fd = fromIntegral . fdSocket $ socket
    theRead key _ = do
      Event.unregisterFd manager key
      bs <- recv socket 4096
      cont bs

initialWrite socket state manager = do
  msg <- readMVar state
  write manager socket msg
  read manager socket $ \ bs -> do
    ByteString.putStrLn bs
    putMVar state msg

Представьте, что есть также некоторые функции, которые добавляют события тайм-аута менеджеру и тому подобное.

Однако этот код не очень приятен, по нескольким причинам:

  • Я провожу менеджер событий вручную.
  • Я должен использовать MVar для моей логики приложения, потому что я не могу сказать непрозрачному менеджеру событий, что он должен передать мне какое-то состояние, хотя я знаю, что он использует только один поток и поэтому может потенциально использоваться как основание стека трансформатора монады.
  • Мне нужно создать явные разграниченные продолжения для чтения (И я мог бы даже сделать это для записи, я не знаю, что было бы мудрее в этой ситуации).

Теперь это просто кричит об использовании множества трансформаторов монады и т.д. Я бы хотел просто сделать это:

main =
  withSocketFromSomewhere $ \ socket ->
  runEvents . flip runStateT "Bla" $ initialWrite socket

initialWrite socket = do
  msg <- lift get
  write socket msg
  resp <- read socket
  liftIO $ ByteString.putStrLn resp
  lift $ put msg

Этот код должен иметь такое же поведение, как и указанный выше код; например путем приостановки вычисления до тех пор, пока чтение не будет получено в строке resp <- read socket и не позволит мне управлять несколькими сокетами в одном и том же потоке/менеджере.

Вопросы:

  • Существует ли более высокоуровневый интерфейс для API/libevent/эквивалента событий GHC, который дает пользователю еще больше возможностей? Стоит ли даже рассматривать синхронные изменения планирования ввода-вывода, которые произошли в недавних GHC (я на 7.4.1)?
  • Что делать, если я хочу реализовать совлокальный concurrency, например, имея одну функцию, которая всегда обрабатывает чтения из сокета, но имея эту функцию совместно использовать одну и ту же государственную монаду в качестве записи "поток"? Можно ли это сделать с любым решением из (1)?

Ответ 1

Я настоятельно рекомендую вам читать Языковой подход к объединению событий и потоков. В нем рассказывается о том, как вы можете структурировать любую систему concurrency, которую вы хотите, в дополнение к своей подсистеме ввода-вывода, и в своей статье они фактически реализуют ее поверх epoll.

К сожалению, типы данных и примеры кода в документе невероятно бедны, и потребовалось некоторое время (по крайней мере, для меня), чтобы перепроектировать их код, и есть даже некоторые ошибки в их документе. Однако их подход на самом деле является подмножеством очень мощного и общего подхода, известного как "свободные монады".

Например, их тип данных Trace - это просто скрытая скрытая монада. Чтобы понять, почему, давайте проконсультироваться с определением свободной монады Хаскелла:

data Free f r = Pure r | Free (f (Free f r))

Свободная монада похожа на "список функторов", где Pure аналогичен конструктору списка Nil, а Free аналогичен конструктору списка Cons, потому что он добавляет дополнительный функтор в "список". Технически, если бы я был педантичным, нет ничего, что говорит о том, что свободная монада должна быть реализована как вышеупомянутый тип данных типа списка, но все, что вы реализуете, должно быть изоморфно вышеуказанному типу данных.

Хорошая вещь о свободной монаде состоит в том, что, учитывая функтор f, Free f автоматически является монадой:

instance (Functor f) => Monad (Free f) where
    return = Pure
    Pure r >>= f = f r
    Free x >>= f = Free (fmap (>>= f) x)

Это означает, что мы можем разложить их тип данных Trace на две части: базовый функтор f, а затем свободную монаду, сгенерированную с помощью f:

-- The base functor
data TraceF x =
    SYS_NBIO (IO x)
  | SYS_FORK x x
  | SYS_YIELD x
  | SYS_RET
  | SYS_EPOLL_WAIT FD EPOLL_EVENT x

-- You can even skip this definition if you use the GHC
-- "DerivingFunctor" extension
instance Functor TraceF where
    fmap f (SYS_NBIO x) = SYS_NBIO (liftM f x)
    fmap f (SYS_FORK x) = SYS_FORK (f x) (f x)
    fmap f (SYS_YIELD x) = SYS_YIELD (f x)
    fmap f SYS_RET = SYS_RET
    fmap f (SYS_EPOLL_WAIT FD EPOLL_EVENT x) = SYS_EPOLL_WAIT FD EPOLL_EVEN (f x)

Учитывая этот функтор, вы получаете Trace монаду "бесплатно":

type Trace a = Free TraceF a
-- or: type Trace = Free TraceF

... хотя это и не потому, что оно называлось "свободной" монадой.

Тогда легче определить все их функции:

liftF = Free . fmap Pure
-- if "Free f" is like a list of "f", then
-- this is sort of like: "liftF x = [x]"
-- it just a convenience function

-- their definitions are written in continuation-passing style,
-- presumably for efficiency, but they are equivalent to these
sys_nbio io = liftF (SYS_NBIO io)
sys_fork t = SYS_FORK t (return ()) -- intentionally didn't use liftF
sys_yield = liftF (SYS_YIELD ())
sys_ret = liftF SYS_RET
sys_epoll_wait fd event = liftF (SYS_EPOLL_WAIT fd event ())

Итак, вы можете использовать эти команды так же, как монада:

myTrace fd event = do
    sys_nbio (putStrLn "Hello, world")
    fork $ do
        sys_nbio (putStrLn "Hey")
    sys_expoll_wait fd event

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

Важная концепция заключается в том, что этот интерпретатор может работать в любой монаде, которую вы хотите. Поэтому, если вы хотите пролить некоторое состояние через ваш concurrency, вы можете это сделать. Например, здесь интерпретатор игрушек, который использует монаду StateT IO для отслеживания того, сколько раз было вызвано действие IO:

interpret t = case t of
    SYS_NBIO io -> do
        modify (+1)
        t' <- lift io
        interpret t'
    ...

Вы можете даже накладывать монады на действия forkIO'd! Вот какой-то очень старый мой код, который багги и хромой, потому что он был написан обратно, когда я был гораздо менее опытным и понятия не имел, что такое свободные монады, но это демонстрирует это в действии:

module Thread (Thread(..), done, lift, branch, fork, run) where

import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad.Cont
import Data.Sequence
import qualified Data.Foldable as F

data Thread f m =
    Done
  | Lift (m (Thread f m))
  | LiftIO (IO (Thread f m))
  | Branch (f (Thread f m))
  | Exit

done = cont $ \c -> Done
lift' x = cont $ \c -> Lift $ liftM c x
liftIO' x = cont $ \c -> LiftIO $ liftM c x
branch x = cont $ \c -> Branch $ fmap c x
exit = cont $ \c -> Exit

fork x = join $ branch [return (), x >> done]

run x = do
    q <- liftIO $ newTChanIO
    enqueue q $ runCont x $ \_ -> Done
    loop q
  where
    loop q = do
        t <- liftIO $ atomically $ readTChan q
        case t of
            Exit -> return ()
            Done -> loop q
            Branch ft -> mapM_ (enqueue q) ft >> loop q
            Lift mt -> (mt >>= enqueue q) >> loop q
            LiftIO it -> (liftIO $ forkIO $ it >>= enqueue q) >> loop q
    enqueue q = liftIO . atomically . writeTChan q

Точка за свободными монадами состоит в том, что они предоставляют экземпляр monad и NOTHING ELSE. Другими словами, они отступают и дают вам полную свободу, как вы хотите их интерпретировать, поэтому они настолько невероятно полезны.