on master help sign in
Refactor/fix returning results from child threads
commit8a0fdb3abd053836c50aa234909a1705ed21df15 authorHarendra Kumar <harendra.kumar@gmail.com> dateSun 25 Jun 2017, 21:23 (9 years ago) parentf49eccb
4 files changed+119−158
src/Strands/AsyncT.hs+87−41 src/Strands/Context.hs+10−16 src/Strands/Threads.hs+17−99 test/2-waitAsync.hs+5−2
src/Strands/AsyncT.hs+87−41
@@ -12,6 +12,10 @@
1212module Strands.AsyncT
1313 ( AsyncT (..)
1414 , waitAsync
15+ , processOneEvent
16+ , drainChildren
17+ , waitForChildren
18+ , getCtxResultDest
1519 , (<**)
1620 , onNothing
1721 , dbg
@@ -21,7 +25,7 @@ where
2125import Control.Applicative (Alternative (..))
2226import Control.Concurrent (ThreadId, killThread)
2327import Control.Concurrent.STM (TChan, atomically, newTChan,
24− readTChan)
28+ readTChan, writeTChan)
2529import Control.Monad.Base (MonadBase (..), liftBaseDefault)
2630import Control.Monad.Catch (MonadCatch, MonadThrow, try,
2731 throwM, SomeException)
@@ -34,14 +38,15 @@ import Control.Monad.Trans.Control (ComposeSt, MonadBaseControl (..),
3438 defaultLiftBaseWith,
3539 defaultRestoreM)
3640import Data.Dynamic (Typeable)
37−import Data.IORef (IORef, newIORef, readIORef,
41+import Data.IORef (IORef, modifyIORef, newIORef, readIORef,
3842 writeIORef)
3943import Data.List (delete)
40−import Data.Maybe (isJust, isNothing)
44+import Data.Maybe (fromJust, isJust, isNothing)
45+import Unsafe.Coerce (unsafeCoerce)
4146
4247import Strands.Context
4348
44−-- import Debug.Trace (traceM)
49+--import Debug.Trace (traceM)
4550
4651newtype AsyncT m a = AsyncT { runAsyncT :: StateT Context m (Maybe a) }
4752
@@ -156,43 +161,82 @@ instance MonadThrow m => MonadThrow (AsyncT m) where
156161-- Thread management
157162------------------------------------------------------------------------------
158163
159−drainChildren :: (MonadIO m, MonadThrow m)
160− => TChan (ChildEvent a) -> [ThreadId] -> [a] -> m [a]
161−drainChildren chan pending res =
162− if pending == []
163− then return res
164− else do
165− ev <- liftIO $ atomically $ readTChan chan
166− case ev of
167− ChildDone tid er -> do
168− dbg $ "drainChildrenTop ChildDone, tid: " ++ show tid
169− case er of
170− Left e -> throwM e
171− Right r ->
172− drainChildren chan (delete tid pending) (r ++ res)
173− PassOnResult er -> do
174− dbg $ "drainChildrenTop PassOnResult"
175− case er of
176− Left e -> throwM e
177− Right r -> drainChildren chan pending (r ++ res)
178−
179−waitForChildren :: (MonadIO m, MonadThrow m)
180− => TChan (ChildEvent a) -> IORef [ThreadId] -> [a] -> m [a]
181−waitForChildren chan pendingRef results = do
164+-- XXX We are using unbounded channels so this will not block on writing to
165+-- pchan. We can use bounded channels to throttle the creation of threads based
166+-- on consumption rate.
167+processOneEvent :: MonadIO m
168+ => ChildEvent a
169+ -> Either (TChan (ChildEvent a)) (IORef [a])
170+ -> [ThreadId]
171+ -> m ([ThreadId], Maybe SomeException)
172+processOneEvent ev dest pending = do
173+ -- Collect results unless we have already encountered an exception.
174+ case ev of
175+ ChildDone _ (Just e) -> handleException e
176+ ChildResult (Left e) -> handleException e
177+ ChildDone tid Nothing -> return (delete tid pending, Nothing)
178+ ChildResult (Right r) -> do
179+ case dest of
180+ Left chan -> liftIO $ atomically $ writeTChan chan ev
181+ Right ref ->
182+ liftIO $ modifyIORef ref $ \rs -> unsafeCoerce r : rs
183+ return (pending, Nothing)
184+ where
185+
186+ handleException e = do
187+ liftIO $ mapM_ killThread pending
188+ return (pending, Just e)
189+
190+drainChildren :: MonadIO m
191+ => Either (TChan (ChildEvent a)) (IORef [a])
192+ -> TChan (ChildEvent a)
193+ -> [ThreadId]
194+ -> m ([ThreadId], Maybe SomeException)
195+drainChildren dest cchan pending =
196+ case pending of
197+ [] -> return (pending, Nothing)
198+ _ -> do
199+ ev <- liftIO $ atomically $ readTChan cchan
200+ (p, e) <- processOneEvent ev dest pending
201+ maybe (drainChildren dest cchan p) (const $ return (p, e)) e
202+
203+waitForChildren :: MonadIO m => Context -> m (Maybe SomeException)
204+waitForChildren ctx = do
205+ let pendingRef = pendingThreads ctx
182206 pending <- liftIO $ readIORef pendingRef
183− r <- drainChildren chan pending results
184− liftIO $ writeIORef pendingRef []
185− return r
207+ (p, e) <- drainChildren (getCtxResultDest ctx) (childChannel ctx) pending
208+ liftIO $ writeIORef pendingRef p
209+ return e
210+
211+getCtxResultDest :: Context -> Either (TChan (ChildEvent a)) (IORef [a])
212+getCtxResultDest ctx =
213+ maybe (Right $ unsafeCoerce $ fromJust $ accumResults ctx)
214+ (Left . unsafeCoerce) (parentChannel ctx)
186215
187216------------------------------------------------------------------------------
188217-- Running the monad
189218------------------------------------------------------------------------------
190219
220+collectResult :: MonadIO m => a -> StateT Context m ()
221+collectResult r = do
222+ ctx <- get
223+ case parentChannel ctx of
224+ Nothing -> do
225+ let ref = fromJust $ accumResults ctx
226+ liftIO $ modifyIORef ref $ \rs -> unsafeCoerce r : rs
227+ Just chan -> do
228+ -- XXX can we pass the result directly to the root thread
229+ -- instead of passing through all the parents? We can let the
230+ -- parent go away and handle the ChildDone events as well in
231+ -- the root thread.
232+ liftIO $ atomically $ writeTChan chan
233+ (ChildResult (Right (unsafeCoerce r)))
234+
191235-- | Invoked to store the result of the computation in the context and finish
192236-- the computation when the computation is done
193−finishComputation :: Monad m => a -> AsyncT m b
237+finishComputation :: MonadIO m => a -> AsyncT m b
194238finishComputation x = AsyncT $ do
195− contextSaveResult x
239+ collectResult x
196240 return Nothing
197241
198242-- XXX pass a collector function and return a Traversable.
@@ -204,21 +248,23 @@ waitAsync :: forall m a. (MonadIO m, MonadCatch m)
204248waitAsync m = do
205249 childChan <- liftIO $ atomically newTChan
206250 pendingRef <- liftIO $ newIORef []
251+ resultsRef <- liftIO $ newIORef []
207252 credit <- liftIO $ newIORef maxBound
208253
209− -- XXX this should be moved to Context.hs and then we can make m
210− -- existential and remove the unsafeCoerces
211− r <- try $ runStateT (runAsyncT m) $ initContext
212− (empty :: AsyncT m a) childChan pendingRef credit
213− finishComputation
254+ let ctx = initContext (empty :: AsyncT m a) childChan pendingRef credit
255+ finishComputation resultsRef
256+
257+ r <- try $ runStateT (runAsyncT $ m >>= finishComputation) ctx
214258
215− xs <- case r of
259+ case r of
216260 Left (exc :: SomeException) -> do
217261 liftIO $ readIORef pendingRef >>= mapM_ killThread
218262 throwM exc
219− Right (Nothing, ctx) -> return $ contextGetResult ctx
220− Right ((Just x), ctx) -> return $ x : contextGetResult ctx
221− waitForChildren childChan pendingRef xs
263+ Right _ -> do
264+ e <- waitForChildren ctx
265+ case e of
266+ Just (exc :: SomeException) -> throwM exc
267+ Nothing -> liftIO $ readIORef resultsRef
222268
223269------------------------------------------------------------------------------
224270-- * Extensible State: Session Data Management
src/Strands/Context.hs+10−16
@@ -9,8 +9,6 @@ module Strands.Context
99 , saveContext
1010 , restoreContext
1111 , resumeContext
12− , contextSaveResult
13− , contextGetResult
1412 , setContextMailBox
1513 , takeContextMailBox
1614 , Location(..)
@@ -40,8 +38,8 @@ import GHC.Prim (Any)
4038------------------------------------------------------------------------------
4139
4240data ChildEvent a =
43− ChildDone ThreadId (Either SomeException [a]) -- A child is finished
44− | PassOnResult (Either SomeException [a]) -- Pass on the result of a child
41+ ChildDone ThreadId (Maybe SomeException) -- A child is finished
42+ | ChildResult (Either SomeException a) -- Pass on the result of a child
4543
4644-- | Describes the context of a computation.
4745data Context = Context
@@ -63,7 +61,12 @@ data Context = Context
6361 , mfData :: M.Map TypeRep Any -- untyped, type coerced
6462 -- ^ State data accessed with get or put operations
6563
64+ -- XXX use Either parentChannel accumResults
65+ , accumResults :: Maybe (IORef [Any])
66+ -- ^ Accumulated results, only the top level thread context accumulates.
67+ -- Child threads just pass on results via the parent channels.
6668 -- XXX we can pass this at the time of fork rather than keeping it here.
69+
6770 , parentChannel :: Maybe (TChan (ChildEvent Any))
6871 -- ^ Our parent thread's channel to communicate to when we die
6972
@@ -104,8 +107,6 @@ data Context = Context
104107 --
105108 , threadCredit :: IORef Int
106109 -- ^ How many more threads are allowed to be created?
107− , accumResults :: [Any]
108− -- ^ Accumulated results when running synchronously
109110 } deriving Typeable
110111
111112initContext
@@ -114,8 +115,9 @@ initContext
114115 -> IORef [ThreadId]
115116 -> IORef Int
116117 -> (b -> m a)
118+ -> IORef [a]
117119 -> Context
118−initContext x childChan pending credit finalizer =
120+initContext x childChan pending credit finalizer results =
119121 Context { mailBox = Nothing
120122 , currentm = unsafeCoerce x
121123 , fstack = [unsafeCoerce finalizer]
@@ -125,7 +127,7 @@ initContext x childChan pending credit finalizer =
125127 , childChannel = unsafeCoerce childChan
126128 , pendingThreads = pending
127129 , threadCredit = credit
128− , accumResults = [] }
130+ , accumResults = Just (unsafeCoerce results) }
129131
130132------------------------------------------------------------------------------
131133-- Where is the computation running?
@@ -199,14 +201,6 @@ resumeContext Context { currentm = m
199201 composefStack [] _ = error "Bug: this should never be reached"
200202 composefStack (f:ff) x = f x >>= composefStack ff
201203
202−contextSaveResult :: Monad m => a -> StateT Context m ()
203−contextSaveResult r =
204− modify $ \ Context {accumResults = rs, ..} ->
205− Context {accumResults = unsafeCoerce r : rs, ..}
206−
207−contextGetResult :: Context -> [a]
208−contextGetResult ctx = unsafeCoerce $ accumResults ctx
209−
210204setContextMailBox :: Context -> a -> Context
211205setContextMailBox ctx mbdata = ctx { mailBox = Just $ unsafeCoerce mbdata }
212206
src/Strands/Threads.hs+17−99
@@ -69,74 +69,20 @@ import Strands.Context
6969-- | Continue execution of the closure that we were executing when we migrated
7070-- to a new thread.
7171
72−resume :: MonadIO m => Context -> StateT Context m ()
73−resume ctx = do
72+runContext :: MonadIO m => Context -> StateT Context m ()
73+runContext ctx = do
7474 -- XXX rename to buildContext or buildState?
7575 let s = runAsyncT (resumeContext ctx)
76− -- The returned value is always 'Nothing', we just discard it
77− (_, c) <- lift $ runStateT s ctx
78−
79− -- XXX can we pass the result directly to the root thread instead of
80− -- passing through all the parents? We can let the parent go away and
81− -- handle the ChildDone events as well in the root thread.
82− case parentChannel c of
83− Nothing -> modify $ \x ->
84− x { accumResults = accumResults c ++ accumResults x }
85− Just chan -> do
86− let r = accumResults c
87− when (length r /= 0) $
88− -- there is only one result in case of a non-root thread
89− -- XXX change the return type to 'a' instead of '[a]'
90− liftIO $ atomically $ writeTChan chan (PassOnResult (Right r))
76+ _ <- lift $ runStateT s ctx
77+ return ()
9178
9279------------------------------------------------------------------------------
9380-- Thread Management (creation, reaping and killing)
9481------------------------------------------------------------------------------
9582
96−-- XXX We are using unbounded channels so this will not block on writing to
97−-- pchan. We can use bounded channels to throttle the creation of threads based
98−-- on consumption rate.
99−processOneEvent :: MonadIO m
100− => ChildEvent a
101− -> TChan (ChildEvent a)
102− -> [ThreadId]
103− -> Maybe SomeException
104− -> m ([ThreadId], Maybe SomeException)
105−processOneEvent ev pchan pending exc = do
106− e <- case exc of
107− Nothing ->
108− case ev of
109− ChildDone tid res -> do
110− dbg $ "processOneEvent ChildDone: " ++ show tid
111− handlePass res
112− PassOnResult res -> do
113− dbg $ "processOneEvent PassOnResult"
114− handlePass res
115− Just _ -> return exc
116− let p = case ev of
117− ChildDone tid _ -> delete tid pending
118− _ -> pending
119− return (p, e)
120−
121− where
122−
123− handlePass :: MonadIO m
124− => Either SomeException [a] -> m (Maybe SomeException)
125− handlePass res =
126− case res of
127− Left e -> do
128− dbg $ "handlePass: caught exception"
129− liftIO $ mapM_ killThread pending
130− return (Just e)
131− Right [] -> return Nothing
132− Right _ -> do
133− liftIO $ atomically $ writeTChan pchan
134− (PassOnResult (unsafeCoerce res))
135− return Nothing
136−
13783tryReclaimZombies :: (MonadIO m, MonadThrow m) => Context -> m ()
13884tryReclaimZombies ctx = do
139− let pchan = fromJust (parentChannel ctx)
85+ let dest = getCtxResultDest ctx
14086 cchan = childChannel ctx
14187 pendingRef = pendingThreads ctx
14288
@@ -148,7 +94,7 @@ tryReclaimZombies ctx = do
14894 case mev of
14995 Nothing -> return ()
15096 Just ev -> do
151− (p, e) <- processOneEvent ev pchan pending Nothing
97+ (p, e) <- processOneEvent ev dest pending
15298 liftIO $ writeIORef pendingRef p
15399 maybe (return ()) throwM e
154100 tryReclaimZombies ctx
@@ -157,40 +103,16 @@ waitForOneEvent :: (MonadIO m, MonadThrow m) => Context -> m ()
157103waitForOneEvent ctx = do
158104 -- XXX assert pending must have at least one element
159105 -- assert that the tid is found in our list
160− let pchan = fromJust (parentChannel ctx)
106+ let dest = getCtxResultDest ctx
161107 cchan = childChannel ctx
162108 pendingRef = pendingThreads ctx
163109
164110 ev <- liftIO $ atomically $ readTChan cchan
165111 pending <- liftIO $ readIORef pendingRef
166− (p, e) <- processOneEvent ev pchan pending Nothing
112+ (p, e) <- processOneEvent ev dest pending
167113 liftIO $ writeIORef pendingRef p
168114 maybe (return ()) throwM e
169115
170−drainChildren :: MonadIO m
171− => TChan (ChildEvent a)
172− -> TChan (ChildEvent a)
173− -> [ThreadId]
174− -> Maybe SomeException
175− -> m (Maybe SomeException)
176−drainChildren pchan cchan pending exc =
177− if pending == []
178− then return exc
179− else do
180− ev <- liftIO $ atomically $ readTChan cchan
181− (p, e) <- processOneEvent ev pchan pending exc
182− drainChildren pchan cchan p e
183−
184−waitForChildren :: MonadIO m
185− => Context -> Maybe SomeException -> m (Maybe SomeException)
186−waitForChildren ctx exc = do
187− let pendingRef = pendingThreads ctx
188− pchan = fromJust (parentChannel ctx)
189− pending <- liftIO $ readIORef pendingRef
190− e <- drainChildren pchan (childChannel ctx) pending exc
191− liftIO $ writeIORef pendingRef []
192− return e
193−
194116-- | kill all the child threads associated with the continuation context
195117killChildren :: Context -> IO ()
196118killChildren ctx = do
@@ -233,7 +155,8 @@ forkFinally1 :: (MonadIO m, MonadBaseControl IO m)
233155forkFinally1 ctx preExit =
234156 EL.mask $ \restore ->
235157 liftBaseWith $ \runInIO -> forkIO $ do
236− _ <- runInIO $ EL.try (restore (resume ctx)) >>= liftIO . preExit
158+ _ <- runInIO $ EL.try (restore (runContext ctx))
159+ >>= liftIO . preExit
237160 return ()
238161
239162-- | Run a given context in a new thread.
@@ -263,27 +186,22 @@ forkContext context = do
263186 { parentChannel = Just (childChannel ctx)
264187 , pendingThreads = pendingRef
265188 , childChannel = chan
266− , accumResults = []
189+ , accumResults = Nothing
267190 }
268191
269192 beforeExit ctx res = do
270193 tid <- myThreadId
271− exc <- case res of
272− Left e -> do
194+ r <- case res of
195+ Left e -> do
273196 dbg $ "beforeExit: " ++ show tid ++ " caught exception"
274197 liftIO $ killChildren ctx
275198 return (Just e)
276− Right _ -> return Nothing
277−
278− e <- waitForChildren ctx exc
199+ Right _ -> waitForChildren ctx
279200
280− -- We are guaranteed to have a parent because we have been explicitly
281− -- forked by some parent.
201+ -- We are guaranteed to have a parent because we are forked.
282202 let p = fromJust (parentChannel ctx)
283203 signalQSemB (threadCredit ctx)
284− -- XXX change the return value type to Maybe SomeException
285− liftIO $ atomically $ writeTChan p
286− (ChildDone tid (maybe (Right []) Left e))
204+ liftIO $ atomically $ writeTChan p (ChildDone tid r)
287205
288206-- | Decide whether to resume the context in the same thread or a new thread
289207--
@@ -318,7 +236,7 @@ resumeContextWith context synch action = do
318236 let ctx = setContextMailBox context (action context)
319237 can <- liftIO $ canFork context
320238 case can && (not synch) of
321− False -> resume ctx -- run synchronously
239+ False -> runContext ctx -- run synchronously
322240 True -> forkContext ctx
323241
324242-- | 'StreamData' represents a task in a task stream being generated.
test/2-waitAsync.hs+5−2
@@ -1,5 +1,8 @@
11import Strands
22import Control.Monad.IO.Class (liftIO)
33
4−main = waitAsync $ do
5− liftIO $ putStrLn "hello"
4+main = do
5+ xs <- waitAsync $ do
6+ liftIO $ putStrLn "hello"
7+ return 5
8+ print xs