我正在进行几个封装在类型别名中的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
中的最终通话对时间非常敏感(且交易具有财务性质)。我认为a
和b
可以并行评估,而懒惰的IO我应该可以这样做:
b <- a `par` connectAPI OtherAPI otherFunction
par
的文档说明Indicates that it may be beneficial to evaluate the first argument in parallel with the second
。
MVar
和liftIO . forkIO
吗?如果我先评估a
,我认为我可以使用eitherT
来检查a
是否成功。但如果我同时评价两者,我会感到困惑。情况如下:
a
失败,我会重试a
,如果失败,我会运行一个手动撤消b
b
失败,我会重试b
,写入RWS
的日志并返回left
RWS
中的日志并返回left
c
(时间不如a
或b
)但如果我同时评估两者,那我怎样才能确定哪一个失败?如果我在eitherT
之后立即使用a
,那么a
将首先评估。如果我在b
之后使用它,那么我将无法判断哪一个失败了。
有没有办法可以并行评估IO调用,但是根据哪一个(如果有的话)失败会有不同的响应?还是我选择了并行与故障缓解?
答案 0 :(得分:2)
您要寻找的解决方案将使用forkIO
和MVar
s。
par
适用于multiprocessor parallelism,它有助于并行评估术语。它对IO
没有帮助。如果你这样做
do
a <- (someProcess :: IO a)
...
到达...
时,IO
行动的所有内容(如果我们忽略了邪恶的懒惰IO)都可以通过普通评估完全确定a
。这意味着,当您执行b <- someOtherProcess
时,someProcess
的所有内容都已完成。和平做任何事都为时已晚。
您可以明确检查Either e a
的{{1}}结果。 EitherT e m a
在底层monad中明确表示成功或失败。我们可以runEitherT :: EitherT e m a -> m (Either e a)
直接进入lift
进行一个总是成功的计算(有时会出错),但有时会失败。
EitherT
在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)
^ ^ ^ ^ ^
行为的结果。如果您分叉堆栈,则需要在将结果重新加入时决定如何处理所有堆栈。您已经决定要明确处理IO
。 Either 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部分)进行检查。您仍然需要决定如何处理来自两个州的数据..