在考虑故障的同时获得并行IO

时间:2015-03-12 04:12:58

标签: haskell

我正在进行几个封装在类型别名中的API调用:

type ConnectT a = EitherT String (RWST ConnectReader ConnectWriter ConnectState IO) a

这是一个函数的简化版本,它连接到两个独立的API:

connectBoth :: ConnectT ()
connectBoth = do
    a <- connectAPI SomeAPI someFunction
    b <- connectAPI OtherAPI otherFunction
    connectAPI OtherAPI (b `sendTo` a)

connectBoth中的最终通话对时间非常敏感(且交易具有财务性质)。我认为ab可以并行评估,而懒惰的IO我应该可以这样做:

    b <- a `par` connectAPI OtherAPI otherFunction

par的文档说明Indicates that it may be beneficial to evaluate the first argument in parallel with the second

  • 这适用于IO吗?
  • 我能否获得更多保证而不是“它可能有益吗?”
  • 或者,如果我需要更多保证,我需要使用MVarliftIO . forkIO吗?

如果我先评估a,我认为我可以使用eitherT来检查a是否成功。但如果我同时评价两者,我会感到困惑。情况如下:

  • 如果只有a失败,我会重试a,如果失败,我会运行一个手动撤消b
  • 的功能
  • 如果只有b失败,我会重试b,写入RWS的日志并返回left
  • 如果两者都无法写入RWS中的日志并返回left
  • 如果两者都成功处理c(时间不如ab

但如果我同时评估两者,那我怎样才能确定哪一个失败?如果我在eitherT之后立即使用a,那么a将首先评估。如果我在b之后使用它,那么我将无法判断哪一个失败了。

有没有办法可以并行评估IO调用,但是根据哪一个(如果有的话)失败会有不同的响应?还是我选择了并行与故障缓解?

1 个答案:

答案 0 :(得分:2)

您要寻找的解决方案将使用forkIOMVar s。

参数

par适用于multiprocessor parallelism,它有助于并行评估术语。它对IO没有帮助。如果你这样做

do
  a <- (someProcess :: IO a)
  ...

到达...时,IO行动的所有内容(如果我们忽略了邪恶的懒惰IO)都可以通过普通评估完全确定a。这意味着,当您执行b <- someOtherProcess时,someProcess的所有内容都已完成。和平做任何事都为时已晚。

EitherT

您可以明确检查Either e a的{​​{1}}结果。 EitherT e m a在底层monad中明确表示成功或失败。我们可以runEitherT :: EitherT e m a -> m (Either e a)直接进入lift进行一个总是成功的计算(有时会出错),但有时会失败。

EitherT

forkIO

import Control.Monad.Trans.Class examine :: (MonadTrans t, Monad m) => EitherT e m a -> t m (Either e a) examine = lift . runEitherT 中执行两项操作的最简单方法是IO。它启动了另一个你可以忘记的轻量级线程。

如果使用变换器堆栈运行值,则完成后将有四个数据。状态forkIO,写入ConnectState日志,计算是否成功,以及取决于它是否成功,无论是值还是错误。

ConnectWriter

如果我们写出这个的结构,它看起来像

EitherT String (RWST ConnectReader ConnectWriter ConnectState IO) a
^       ^                          ^             ^                ^

所有这四条信息最终都归结为(RWST ConnectReader ConnectWriter ConnectState IO) (Either String a) ^ ^ ^ ^ ^ ConnectReader -> ConnectState -> IO (Either String a, ConnectState, ConnectWriter) ^ ^ ^ ^ ^ 行为的结果。如果您分叉堆栈,则需要在将结果重新加入时决定如何处理所有堆栈。您已经决定要明确处理IOEither String a可能与ConnectWriter组合在一起。您需要决定如何处理<>

我们制作一个ConnectState,通过将它们推送到fork来返回所有这四个数据。

MVar

稍后,当我们想要结果时,我们可以import Control.Concurrent import Control.Concurrent.MVar import Control.Monad.IO.Class forkConnectT :: ConnectT a -> ConnectT (MVar (Either String a, ConnectState, ConnectWriter)) forkConnectT cta = do result <- liftIO newEmptyMVar r <- lift ask s <- lift get liftIO $ forkIO $ do state <- runRWST (runEitherT cta) r s putMVar result state return result 并查看它是否已准备就绪。在处理幕后的状态和作者时,我们会明确地处理成功和失败的try

Either

在幕后我们import Data.Traversable tryJoinConnectT :: MVar (Either String a, ConnectState, ConnectWriter) -> ConnectT (Maybe (Either String a)) tryJoinConnectT result = liftIO (tryTakeMVar result) >>= traverse reintegrate reintegrate告诉ConnectWriter写下在另一个帖子中累积的内容。您需要决定如何将这两种状态结合起来。

ConnectT

如果我们想等到结果准备就绪,我们可以阻止阅读reintegrate :: (a, ConnectState, ConnectWriter) -> ConnectT a reintegrate (a, s, w) = do -- Whatever needs to be done with the state. -- stateHere <- lift get lift $ tell w return a 。这样可以减少处理诸如超时等错误的机会。

MVar

实施例

总而言之,我们可以并行分叉任务,在此线程中执行某些操作,明确检查成功或失败,使用其他线程的结果加入,并使用显式{{1}推断下一步操作的原因表示每个过程的成功或失败。

joinConnectT :: MVar (Either String a, ConnectState, ConnectWriter) -> ConnectT (Either String a)
joinConnectT result = liftIO (takeMVar result) >>= reintegrate

走得更远

如果你是偏执狂,你也会想要处理异常(其中一些可以由forkFinally)和asynchronous exceptions处理。您需要决定是bundle these exceptions进入您的堆栈还是处理Either,因为它总是会抛出异常。

考虑使用async代替connectBoth :: ConnectT () connectBoth = do bVar <- forkConnectT $ connectAPI OtherAPI otherFunction a <- examine $ connectAPI SomeAPI someFunction b <- joinConnectT bVar ... IO s。

monad-control(你已经依赖于via either)提供了一次建立一个变换器的机制,一个表示monad变换器堆栈状态的类型。我们手写为forkIO。如果要增加变换器堆栈,可能需要从MVar获取此变量。您可以在父级中restore the state from the forked thread(请参阅MonadBaseControl部分)进行检查。您仍然需要决定如何处理来自两个州的数据..