on master help sign in
Alternative now forks automatically

Alternative "can" fork an action `async` primitive is used to force an action to be async

commitf5cfead136ebe37d627604bde8739afb0b1dccde authorHarendra Kumar <harendra.kumar@gmail.com> dateSat 8 Jul 2017, 07:31 (9 years ago) parent6c12266
7 files changed+436−403
src/Asyncly.hs+13−1 src/Asyncly/AsyncT.hs+332−22 src/Asyncly/Threads.hs+16−348 test/3-async.hs+2−2 test/3-sample.hs+1−1 test/Main.hs+69−26 test/alternative.hs+3−3
src/Asyncly.hs+13−1
@@ -8,6 +8,18 @@
88-- Portability : GHC
99--
1010
11+-- | Asyncly allows writing and composing asynchronous event driven
12+-- applications conveniently. It can be thought of as a non-deterministic
13+-- continuation monad or a combination of ContT and ListT. It allows to capture
14+-- the state of the application at any point and trigger arbitrary number of
15+-- continuations from the capture point. Non-determinism or parallel
16+-- continuations are introduced using the <|> operator. It provides a
17+-- convenient way to implement and compose state machines without using
18+-- callbacks. An event in the state machine corresponds to a continuation.
19+-- There are no cycles in the state machine as each transition in the state
20+-- machine is an independent instance of the state machine. It is an immutable
21+-- state machine!
22+
1123module Asyncly
1224 ( AsyncT
1325 , MonadAsync
@@ -31,5 +43,5 @@ module Asyncly
3143 )
3244where
3345
34−import Asyncly.AsyncT (AsyncT, Log, Loggable)
46+import Asyncly.AsyncT
3547import Asyncly.Threads
src/Asyncly/AsyncT.hs+332−22
@@ -12,6 +12,7 @@
1212
1313module Asyncly.AsyncT
1414 ( AsyncT (..)
15+ , MonadAsync
1516 , ChildEvent(..)
1617 , Loggable
1718 , Log (..)
@@ -22,16 +23,29 @@ module Asyncly.AsyncT
2223 , Location(..)
2324 , getLocation
2425 , setLocation
26+ , threads
27+ , waitForChildren
28+
29+ , async
30+ , makeAsync
31+ , each
2532 , (<**)
2633 , (**>)
34+
35+ -- internal
2736 , dbg
2837 )
2938where
3039
3140import Control.Applicative (Alternative (..))
32−import Control.Concurrent (ThreadId)
33−import Control.Concurrent.STM (TChan)
34−import Control.Exception (SomeException)
41+import Control.Concurrent (ThreadId, forkIO, killThread,
42+ myThreadId)
43+import Control.Concurrent.STM (TChan, atomically, newTChan,
44+ readTChan, tryReadTChan,
45+ writeTChan)
46+import Control.Exception (SomeException (..))
47+import qualified Control.Exception.Lifted as EL
48+import Control.Monad (ap, mzero, when)
3549import Control.Monad.Base (MonadBase (..), liftBaseDefault)
3650import Control.Monad.Catch (MonadThrow, throwM)
3751import Control.Monad.State (MonadIO (..), MonadPlus (..),
@@ -41,8 +55,12 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
4155import Control.Monad.Trans.Control (ComposeSt, MonadBaseControl (..),
4256 MonadTransControl (..),
4357 defaultLiftBaseWith,
44− defaultRestoreM)
45−import Data.IORef (IORef)
58+ defaultRestoreM, liftBaseWith)
59+import Data.IORef (IORef, atomicModifyIORef,
60+ modifyIORef, newIORef, readIORef,
61+ writeIORef)
62+import Data.List (delete)
63+import Data.Maybe (isJust)
4664import GHC.Prim (Any)
4765import Unsafe.Coerce (unsafeCoerce)
4866
@@ -189,41 +207,333 @@ setLocation loc = modify $ \Context { .. } -> Context { location = loc, .. }
189207dbg :: Monad m => a -> m ()
190208dbg _ = return ()
191209
210+type MonadAsync m = (MonadIO m, MonadBaseControl IO m, MonadThrow m)
211+
212+------------------------------------------------------------------------------
213+-- Pick up from where we left in the previous thread
214+------------------------------------------------------------------------------
215+
216+-- | Run the continuation of an action
217+runContext :: MonadAsync m => Context -> AsyncT m a -> StateT Context m ()
218+runContext ctx action = do
219+ let s = runAsyncT $ action >>= unsafeCoerce (continuation ctx)
220+ _ <- lift $ runStateT s ctx
221+ return ()
222+
223+------------------------------------------------------------------------------
224+-- Thread Management (creation, reaping and killing)
225+------------------------------------------------------------------------------
226+
227+-- XXX We are using unbounded channels so this will not block on writing to
228+-- pchan. We can use bounded channels to throttle the creation of threads based
229+-- on consumption rate.
230+processOneEvent :: MonadIO m
231+ => ChildEvent
232+ -> [ThreadId]
233+ -> m ([ThreadId], Maybe SomeException)
234+processOneEvent (ChildDone tid e) pending = do
235+ when (isJust e) $ liftIO $ mapM_ killThread pending
236+ return (delete tid pending, Nothing)
237+
238+drainChildren :: MonadIO m
239+ => TChan ChildEvent
240+ -> [ThreadId]
241+ -> m ([ThreadId], Maybe SomeException)
242+drainChildren cchan pending =
243+ case pending of
244+ [] -> return (pending, Nothing)
245+ _ -> do
246+ ev <- liftIO $ atomically $ readTChan cchan
247+ (p, e) <- processOneEvent ev pending
248+ maybe (drainChildren cchan p) (const $ return (p, e)) e
249+
250+waitForChildren :: MonadIO m => Context -> m (Maybe SomeException)
251+waitForChildren ctx = do
252+ let pendingRef = pendingThreads ctx
253+ pending <- liftIO $ readIORef pendingRef
254+ (p, e) <- drainChildren (childChannel ctx) pending
255+ liftIO $ writeIORef pendingRef p
256+ return e
257+
258+tryReclaimZombies :: (MonadIO m, MonadThrow m) => Context -> m ()
259+tryReclaimZombies ctx = do
260+ let cchan = childChannel ctx
261+ pendingRef = pendingThreads ctx
262+
263+ pending <- liftIO $ readIORef pendingRef
264+ case pending of
265+ [] -> return ()
266+ _ -> do
267+ mev <- liftIO $ atomically $ tryReadTChan cchan
268+ case mev of
269+ Nothing -> return ()
270+ Just ev -> do
271+ (p, e) <- processOneEvent ev pending
272+ liftIO $ writeIORef pendingRef p
273+ maybe (return ()) throwM e
274+ tryReclaimZombies ctx
275+
276+waitForOneEvent :: (MonadIO m, MonadThrow m) => Context -> m ()
277+waitForOneEvent ctx = do
278+ -- XXX assert pending must have at least one element
279+ -- assert that the tid is found in our list
280+ let cchan = childChannel ctx
281+ pendingRef = pendingThreads ctx
282+
283+ ev <- liftIO $ atomically $ readTChan cchan
284+ pending <- liftIO $ readIORef pendingRef
285+ (p, e) <- processOneEvent ev pending
286+ liftIO $ writeIORef pendingRef p
287+ maybe (return ()) throwM e
288+
289+-- XXX this is not a real semaphore as it does not really block on wait,
290+-- instead it returns whether the value is zero or non-zero.
291+--
292+waitQSemB :: IORef Int -> IO Bool
293+waitQSemB sem = atomicModifyIORef sem $ \n ->
294+ if n > 0
295+ then (n - 1, True)
296+ else (n, False)
297+
298+signalQSemB :: IORef Int -> IO ()
299+signalQSemB sem = atomicModifyIORef sem $ \n -> (n + 1, ())
300+
301+-- Allocation of threads
302+--
303+-- global thread limit
304+-- thread fan-out i.e. per thread children limit
305+-- min per thread allocation to avoid starvation
306+--
307+-- dynamic adjustment based on the cost, speed of consumption, cpu utilization
308+-- etc. We need to adjust the limits based on cost, throughput and latencies.
309+--
310+-- The event producer thread must put the work on a work-queue and the child
311+-- threads can pick it up from there. But if there is just one consumer then it
312+-- may not make sense to have a separate producer unless the producing cost is
313+-- high.
314+--
315+
316+forkFinally1 :: MonadAsync m
317+ => Context
318+ -> AsyncT m a
319+ -> (Either SomeException () -> IO ())
320+ -> StateT Context m ThreadId
321+forkFinally1 ctx action preExit =
322+ EL.mask $ \restore ->
323+ liftBaseWith $ \runInIO -> forkIO $ do
324+ _ <- runInIO $ EL.try (restore (runContext ctx action))
325+ >>= liftIO . preExit
326+ -- XXX restore state here
327+ return ()
328+
329+-- | Run a given context in a new thread.
330+--
331+forkContext :: (MonadBaseControl IO m, MonadIO m, MonadThrow m)
332+ => AsyncT m a -> StateT Context m ()
333+forkContext action = do
334+ parentCtx <- get
335+ childCtx <- childContext parentCtx
336+ tid <- forkFinally1 childCtx action
337+ (beforeExit childCtx (childChannel parentCtx))
338+ updatePendingThreads parentCtx tid
339+
340+ where
341+
342+ updatePendingThreads :: (MonadIO m, MonadThrow m)
343+ => Context -> ThreadId -> m ()
344+ updatePendingThreads ctx tid = do
345+ -- update the new thread before reclaiming zombies so that if it exited
346+ -- already reclaim finds it in the list and does not panic.
347+ liftIO $ modifyIORef (pendingThreads ctx) $ (\ts -> tid:ts)
348+ tryReclaimZombies ctx
349+
350+ childContext ctx = do
351+ pendingRef <- liftIO $ newIORef []
352+ chan <- liftIO $ atomically newTChan
353+ -- shares the threadCredit of the parent by default
354+ return $ ctx
355+ { pendingThreads = pendingRef
356+ , childChannel = chan
357+ }
358+
359+ beforeExit ctx pchan res = do
360+ tid <- myThreadId
361+ r <- case res of
362+ Left e -> do
363+ dbg $ "beforeExit: " ++ show tid ++ " caught exception"
364+ liftIO $ readIORef (pendingThreads ctx) >>= mapM_ killThread
365+ return (Just e)
366+ Right _ -> waitForChildren ctx
367+
368+ signalQSemB (threadCredit ctx)
369+ liftIO $ atomically $ writeTChan pchan (ChildDone tid r)
370+
371+-- | Decide whether to resume the context in the same thread or a new thread
372+--
373+canFork :: Context -> IO Bool
374+canFork context = do
375+ gotCredit <- liftIO $ waitQSemB (threadCredit context)
376+ case gotCredit of
377+ False -> do
378+ pending <- liftIO $ readIORef $ pendingThreads context
379+ case pending of
380+ [] -> return False
381+ _ -> do
382+ -- XXX If we have unreclaimable child threads e.g.
383+ -- infinite loop, this is going to deadlock us. We need
384+ -- special handling for those cases. Add those to
385+ -- unreclaimable list? And always execute them in an
386+ -- async thread, cannot use sync for those.
387+ --
388+ waitForOneEvent context
389+ canFork context
390+ True -> return True
391+
392+-- Housekeeping, invoked after spawning of all child tasks is done and the
393+-- parent task needs to terminate. Either the task is fully done or we handed
394+-- it over to another thread, in any case the current thread is done.
395+
396+spawningParentDone :: MonadIO m => StateT Context m (Maybe a)
397+spawningParentDone = do
398+ loc <- getLocation
399+ when (loc /= RemoteNode) $ setLocation WaitingParent
400+ return Nothing
401+
402+-- | The task may be run in the same thread or in a new thread depending on the
403+-- forceAsync parameter and the current thread quota.
404+--
405+runAsyncTask :: MonadAsync m
406+ => Bool
407+ -> AsyncT m a
408+ -> StateT Context m (Maybe a)
409+runAsyncTask forceAsync action = do
410+ if forceAsync
411+ then forkContext action
412+ else do
413+ context <- get
414+ can <- liftIO $ canFork context
415+ case can of
416+ False -> runContext context action
417+ True -> forkContext action
418+ spawningParentDone
419+
420+-- The current model is to start a new thread for every task. The input is
421+-- provided at the time of the creation and therefore no synchronization is
422+-- needed compared to a pool of threads contending to get the input from a
423+-- channel. However the thread creation overhead may be more than the
424+-- synchronization cost?
425+--
426+-- When the task is over the outputs need to be collected and that requires
427+-- synchronization irrespective of a thread pool model or per task new thread
428+-- model.
429+--
430+-- XXX instead of starting a new thread every time, reuse the existing child
431+-- threads and send them work via a shared channel. When there is no more work
432+-- available we need a way to close the channel and wakeup all waiters so that
433+-- they can go away rather than waiting indefinitely.
434+--
435+
436+-- Only those actions that are marked with 'async' are guaranteed to be
437+-- asynchronous. Asyncly is free to run other actions synchronously or
438+-- asynchronously and it should not matter to the semantics of the program, if
439+-- it does then use async to force.
440+--
441+-- Why not make async as default and ask the programmer to use a 'sync'
442+-- primitive to force an action to run synchronously? But then we would not
443+-- have the freedom to convert the async actions to sync dynamically. Note that
444+-- it is safe to convert a sync action to async but vice-versa is not true.
445+-- Converting an async to sync can cause change in semantics if the async
446+-- action was an infinite loop for example.
447+--
448+-- | In an 'Alternative' composition, force the action to run asynchronously.
449+-- The <|> operator implies "can be parallel", whereas async implies "must be
450+-- parallel". Note that async is not useful and should never be used outside an
451+-- 'Alternative' composition. Even in an 'Alternative' composition 'async' is
452+-- not useful in the last action.
453+async :: MonadAsync m => AsyncT m a -> AsyncT m a
454+async action = AsyncT $ runAsyncTask True action
455+
456+-- XXX After setting the callback we should wait for the callback otherwise we
457+-- will just go away. We should use the same finalization mechanism that we use
458+-- in fork.
459+--
460+-- | Makes an asyncly action from a callback setter function; can be used to
461+-- convert existing callback style code into asyncly style code. The first
462+-- parameter is a callback setter function. When we are called back,
463+-- 'makeAsync' behaves as if it was an async computation that just returned a
464+-- value of type 'a'. After the computation is done the result of the action
465+-- in second parameter is returned to the callback.
466+--
467+makeAsync :: MonadAsync m => ((a -> m ()) -> m ()) -> AsyncT m a
468+makeAsync cbsetter = AsyncT $ do
469+ -- XXX should we force fork a thread or keep running in the context of the
470+ -- callback?
471+ ctx <- get
472+ lift $ cbsetter $ \a -> do
473+ let s = runAsyncTask False (return a)
474+ _ <- runStateT s ctx
475+ return ()
476+ spawningParentDone
477+
478+-- scatter
479+each :: (MonadIO m, MonadBaseControl IO m, MonadThrow m)
480+ => [a] -> AsyncT m a
481+each xs = foldl (<|>) empty $ map return xs
482+
483+------------------------------------------------------------------------------
484+-- Controlling thread quota
485+------------------------------------------------------------------------------
486+
487+-- XXX Should n be Word32 instead?
488+-- | Runs a computation under a given thread limit. A limit of 0 means new
489+-- tasks start synchronously in the current thread. New threads are created by
490+-- 'parallel', and APIs that use parallel.
491+threads :: MonadIO m => Int -> AsyncT m a -> AsyncT m a
492+threads n process = AsyncT $ do
493+ oldCr <- gets threadCredit
494+ newCr <- liftIO $ newIORef n
495+ modify $ \s -> s { threadCredit = newCr }
496+ r <- runAsyncT $ process
497+ <** (AsyncT $ do
498+ modify $ \s -> s { threadCredit = oldCr }
499+ return (Just ())
500+ ) -- restore old credit
501+ return r
502+
192503------------------------------------------------------------------------------
193504-- Functor
194505------------------------------------------------------------------------------
195506
196−instance Monad (AsyncT m) => Functor (AsyncT m) where
197− fmap f mx = do
198− x <- mx
199− return $ f x
507+instance MonadAsync m => Functor (AsyncT m) where
508+ fmap = liftM
200509
201510------------------------------------------------------------------------------
202511-- Applicative
203512------------------------------------------------------------------------------
204513
205−instance Monad m => Applicative (AsyncT m) where
514+
515+instance MonadAsync m => Applicative (AsyncT m) where
206516 pure a = AsyncT . return $ Just a
207− m1 <*> m2 = do { x1 <- m1; x2 <- m2; return (x1 x2) }
517+ (<*>) = ap
208518
209519------------------------------------------------------------------------------
210520-- Alternative
211521------------------------------------------------------------------------------
212522
213−instance Monad m => Alternative (AsyncT m) where
523+instance MonadAsync m => Alternative (AsyncT m) where
214524 empty = AsyncT $ return Nothing
215− (<|>) x y = AsyncT $ do
216− mx <- runAsyncT x
525+ (<|>) m1 m2 = AsyncT $ do
526+ r <- runAsyncTask False m1
217527 loc <- getLocation
218528 case loc of
219529 RemoteNode -> return Nothing
220− _ -> maybe (runAsyncT y) (return . Just) mx
530+ _ -> maybe (runAsyncT m2) (return . Just) r
221531
222532------------------------------------------------------------------------------
223533-- Monad
224534------------------------------------------------------------------------------
225535
226−instance Monad m => Monad (AsyncT m) where
536+instance MonadAsync m => Monad (AsyncT m) where
227537 return = pure
228538 m >>= f = AsyncT $ do
229539 modify $ \Context { continuation = g, .. } ->
@@ -234,11 +544,11 @@ instance Monad m => Monad (AsyncT m) where
234544 Context { continuation = g } <- get
235545 runAsyncT $ maybe mzero (unsafeCoerce g) x
236546
237−instance Monad m => MonadPlus (AsyncT m) where
547+instance MonadAsync m => MonadPlus (AsyncT m) where
238548 mzero = empty
239549 mplus = (<|>)
240550
241−instance (Monoid a, Monad m) => Monoid (AsyncT m a) where
551+instance (Monoid a, MonadAsync m) => Monoid (AsyncT m a) where
242552 mappend x y = mappend <$> x <*> y
243553 mempty = return mempty
244554
@@ -246,7 +556,7 @@ instance (Monoid a, Monad m) => Monoid (AsyncT m a) where
246556-- MonadIO
247557------------------------------------------------------------------------------
248558
249−instance MonadIO m => MonadIO (AsyncT m) where
559+instance MonadAsync m => MonadIO (AsyncT m) where
250560 liftIO mx = AsyncT $ liftIO mx >>= return . Just
251561
252562-------------------------------------------------------------------------------
@@ -265,17 +575,17 @@ instance MonadTransControl AsyncT where
265575 {-# INLINABLE liftWith #-}
266576 {-# INLINABLE restoreT #-}
267577
268−instance (MonadBase b m, MonadIO m) => MonadBase b (AsyncT m) where
578+instance (MonadBase b m, MonadAsync m) => MonadBase b (AsyncT m) where
269579 liftBase = liftBaseDefault
270580
271−instance (MonadBaseControl b m, MonadIO m) => MonadBaseControl b (AsyncT m) where
581+instance (MonadBaseControl b m, MonadAsync m) => MonadBaseControl b (AsyncT m) where
272582 type StM (AsyncT m) a = ComposeSt AsyncT m a
273583 liftBaseWith = defaultLiftBaseWith
274584 restoreM = defaultRestoreM
275585 {-# INLINABLE liftBaseWith #-}
276586 {-# INLINABLE restoreM #-}
277587
278−instance MonadThrow m => MonadThrow (AsyncT m) where
588+instance MonadAsync m => MonadThrow (AsyncT m) where
279589 throwM e = lift $ throwM e
280590
281591------------------------------------------------------------------------------
src/Asyncly/Threads.hs+16−348
@@ -1,16 +1,9 @@
1−{-# LANGUAGE ConstraintKinds #-}
21{-# LANGUAGE FlexibleContexts #-}
32{-# LANGUAGE ScopedTypeVariables #-}
43
54module Asyncly.Threads
6− ( MonadAsync
7− , wait
5+ ( wait
86 , wait_
9− , threads
10−
11− , async
12− , makeAsync
13− , each
147 , gather
158
169 , waitLogged
@@ -22,349 +15,25 @@ module Asyncly.Threads
2215 )
2316where
2417
25−import Control.Applicative ((<|>), empty)
26−import Control.Concurrent (ThreadId, forkIO, killThread,
27− myThreadId)
28−import Control.Concurrent.STM (TChan, atomically, newTChan,
29− readTChan, tryReadTChan,
30− writeTChan)
18+import Control.Concurrent (killThread)
19+import Control.Concurrent.STM (atomically, newTChan)
3120import Control.Exception (SomeException (..))
32−import qualified Control.Exception.Lifted as EL
33−import Control.Monad.Catch (MonadCatch, MonadThrow, throwM,
34− try)
21+import Control.Monad.Catch (MonadCatch, throwM, try)
3522import Control.Monad.IO.Class (MonadIO (..))
36−import Control.Monad.State (get, gets, modify, mzero, put,
37− runStateT, when, StateT)
23+import Control.Monad.State (get, mzero, put, runStateT)
3824import Control.Monad.Trans.Class (MonadTrans (lift))
39−import Control.Monad.Trans.Control (MonadBaseControl, liftBaseWith)
4025import Data.IORef (IORef, atomicModifyIORef,
41− modifyIORef, newIORef, readIORef,
42− writeIORef)
43−import Data.List (delete)
44−import Data.Maybe (isJust)
45−import Unsafe.Coerce (unsafeCoerce)
26+ newIORef, readIORef)
4627
4728import Asyncly.AsyncT
4829
49−type MonadAsync m = (MonadIO m, MonadBaseControl IO m, MonadThrow m)
50−
51−------------------------------------------------------------------------------
52−-- Model of computation
53−------------------------------------------------------------------------------
54−
55−-- A computation starts in a top level thread. If no "forking primitives" are
56−-- used then the thread finishes in a straight line flow just like the IO
57−-- monad. However, if a "forking primitive" is used it "forks" the computation
58−-- into multiple flows. The "forked" computations may run concurrently (and
59−-- therefore in parallel when possible) or serially. When multiple forking
60−-- primitives are composed it results into a tree of computations where each
61−-- branch of the tree can run concurrently.
62−--
63−-- A forking primitive may create multiple forks at that point, each fork
64−-- provides a specific input value to be used in the forked computation, this
65−-- value defines the fork.
66−--
67−-- The final result of the computation is the collection of all the values
68−-- generated by all the leaf level forks. These values are then propagated up
69−-- the tree and collected at the root of the tree.
70−--
71−-- Since AsyncT is a transformer we can use things like pipe, conduit or any
72−-- other transformer monads inside the computations to utilize single threaded
73−-- composition or data flow techniques.
74−--
75−------------------------------------------------------------------------------
76−-- Pick up from where we left in the previous thread
77−------------------------------------------------------------------------------
78−
79−-- | Continue execution of the closure that we were executing when we migrated
80−-- to a new thread.
81−
82−runContext :: Monad m => Context -> AsyncT m a -> StateT Context m ()
83−runContext ctx action = do
84− let s = runAsyncT $ action >>= unsafeCoerce (continuation ctx)
85− _ <- lift $ runStateT s ctx
86− return ()
87−
88−------------------------------------------------------------------------------
89−-- Thread Management (creation, reaping and killing)
90−------------------------------------------------------------------------------
91−
92−-- XXX We are using unbounded channels so this will not block on writing to
93−-- pchan. We can use bounded channels to throttle the creation of threads based
94−-- on consumption rate.
95−processOneEvent :: MonadIO m
96− => ChildEvent
97− -> [ThreadId]
98− -> m ([ThreadId], Maybe SomeException)
99−processOneEvent (ChildDone tid e) pending = do
100− when (isJust e) $ liftIO $ mapM_ killThread pending
101− return (delete tid pending, Nothing)
102−
103−drainChildren :: MonadIO m
104− => TChan ChildEvent
105− -> [ThreadId]
106− -> m ([ThreadId], Maybe SomeException)
107−drainChildren cchan pending =
108− case pending of
109− [] -> return (pending, Nothing)
110− _ -> do
111− ev <- liftIO $ atomically $ readTChan cchan
112− (p, e) <- processOneEvent ev pending
113− maybe (drainChildren cchan p) (const $ return (p, e)) e
114−
115−waitForChildren :: MonadIO m => Context -> m (Maybe SomeException)
116−waitForChildren ctx = do
117− let pendingRef = pendingThreads ctx
118− pending <- liftIO $ readIORef pendingRef
119− (p, e) <- drainChildren (childChannel ctx) pending
120− liftIO $ writeIORef pendingRef p
121− return e
122−
123−tryReclaimZombies :: (MonadIO m, MonadThrow m) => Context -> m ()
124−tryReclaimZombies ctx = do
125− let cchan = childChannel ctx
126− pendingRef = pendingThreads ctx
127−
128− pending <- liftIO $ readIORef pendingRef
129− case pending of
130− [] -> return ()
131− _ -> do
132− mev <- liftIO $ atomically $ tryReadTChan cchan
133− case mev of
134− Nothing -> return ()
135− Just ev -> do
136− (p, e) <- processOneEvent ev pending
137− liftIO $ writeIORef pendingRef p
138− maybe (return ()) throwM e
139− tryReclaimZombies ctx
140−
141−waitForOneEvent :: (MonadIO m, MonadThrow m) => Context -> m ()
142−waitForOneEvent ctx = do
143− -- XXX assert pending must have at least one element
144− -- assert that the tid is found in our list
145− let cchan = childChannel ctx
146− pendingRef = pendingThreads ctx
147−
148− ev <- liftIO $ atomically $ readTChan cchan
149− pending <- liftIO $ readIORef pendingRef
150− (p, e) <- processOneEvent ev pending
151− liftIO $ writeIORef pendingRef p
152− maybe (return ()) throwM e
153−
154−-- XXX this is not a real semaphore as it does not really block on wait,
155−-- instead it returns whether the value is zero or non-zero.
156−--
157−waitQSemB :: IORef Int -> IO Bool
158−waitQSemB sem = atomicModifyIORef sem $ \n ->
159− if n > 0
160− then (n - 1, True)
161− else (n, False)
162−
163−signalQSemB :: IORef Int -> IO ()
164−signalQSemB sem = atomicModifyIORef sem $ \n -> (n + 1, ())
165−
166−-- Allocation of threads
167−--
168−-- global thread limit
169−-- thread fan-out i.e. per thread children limit
170−-- min per thread allocation to avoid starvation
171−--
172−-- dynamic adjustment based on the cost, speed of consumption, cpu utilization
173−-- etc. We need to adjust the limits based on cost, throughput and latencies.
174−--
175−-- The event producer thread must put the work on a work-queue and the child
176−-- threads can pick it up from there. But if there is just one consumer then it
177−-- may not make sense to have a separate producer unless the producing cost is
178−-- high.
179−--
180−
181−forkFinally1 :: (MonadIO m, MonadBaseControl IO m)
182− => Context
183− -> AsyncT m a
184− -> (Either SomeException () -> IO ())
185− -> StateT Context m ThreadId
186−forkFinally1 ctx action preExit =
187− EL.mask $ \restore ->
188− liftBaseWith $ \runInIO -> forkIO $ do
189− _ <- runInIO $ EL.try (restore (runContext ctx action))
190− >>= liftIO . preExit
191− -- XXX restore state here
192− return ()
193−
194−-- | Run a given context in a new thread.
195−--
196−forkContext :: (MonadBaseControl IO m, MonadIO m, MonadThrow m)
197− => AsyncT m a -> StateT Context m ()
198−forkContext action = do
199− parentCtx <- get
200− childCtx <- childContext parentCtx
201− tid <- forkFinally1 childCtx action
202− (beforeExit childCtx (childChannel parentCtx))
203− updatePendingThreads parentCtx tid
204−
205− where
206−
207− updatePendingThreads :: (MonadIO m, MonadThrow m)
208− => Context -> ThreadId -> m ()
209− updatePendingThreads ctx tid = do
210− -- update the new thread before reclaiming zombies so that if it exited
211− -- already reclaim finds it in the list and does not panic.
212− liftIO $ modifyIORef (pendingThreads ctx) $ (\ts -> tid:ts)
213− tryReclaimZombies ctx
214−
215− childContext ctx = do
216− pendingRef <- liftIO $ newIORef []
217− chan <- liftIO $ atomically newTChan
218− -- shares the threadCredit of the parent by default
219− return $ ctx
220− { pendingThreads = pendingRef
221− , childChannel = chan
222− }
223−
224− beforeExit ctx pchan res = do
225− tid <- myThreadId
226− r <- case res of
227− Left e -> do
228− dbg $ "beforeExit: " ++ show tid ++ " caught exception"
229− liftIO $ readIORef (pendingThreads ctx) >>= mapM_ killThread
230− return (Just e)
231− Right _ -> waitForChildren ctx
232−
233− signalQSemB (threadCredit ctx)
234− liftIO $ atomically $ writeTChan pchan (ChildDone tid r)
235−
236−-- | Decide whether to resume the context in the same thread or a new thread
237−--
238−canFork :: Context -> IO Bool
239−canFork context = do
240− gotCredit <- liftIO $ waitQSemB (threadCredit context)
241− case gotCredit of
242− False -> do
243− pending <- liftIO $ readIORef $ pendingThreads context
244− case pending of
245− [] -> return False
246− _ -> do
247− -- XXX If we have unreclaimable child threads e.g.
248− -- infinite loop, this is going to deadlock us. We need
249− -- special handling for those cases. Add those to
250− -- unreclaimable list? And always execute them in an
251− -- async thread, cannot use sync for those.
252− --
253− waitForOneEvent context
254− canFork context
255− True -> return True
256−
257−-- | Resume a captured context with a given action. The context may be resumed
258−-- in the same thread or in a new thread depending on the synch parameter and
259−-- the current thread quota.
260−--
261−resumeContextWith :: MonadAsync m
262− => Bool -- force synchronous
263− -> AsyncT m a
264− -> StateT Context m ()
265−resumeContextWith synch action = do
266− context <- get
267− can <- liftIO $ canFork context
268− case can && (not synch) of
269− False -> runContext context action
270− True -> forkContext action
271−
272−-- The current model is to start a new thread for every task. The input is
273−-- provided at the time of the creation and therefore no synchronization is
274−-- needed compared to a pool of threads contending to get the input from a
275−-- channel. However the thread creation overhead may be more than the
276−-- synchronization cost?
277−--
278−-- When the task is over the outputs need to be collected and that requires
279−-- synchronization irrespective of a thread pool model or per task new thread
280−-- model.
281−--
282−-- XXX instead of starting a new thread every time, reuse the existing child
283−-- threads and send them work via a shared channel. When there is no more work
284−-- available we need a way to close the channel and wakeup all waiters so that
285−-- they can go away rather than waiting indefinitely.
286−--
287−
288−-- Housekeeping, invoked after spawning of all child tasks is done and the
289−-- parent task needs to terminate. Either the task is fully done or we handed
290−-- it over to another thread, in any case the current thread is done.
291−
292−spawningParentDone :: MonadIO m => StateT Context m (Maybe a)
293−spawningParentDone = do
294− loc <- getLocation
295− when (loc /= RemoteNode) $ setLocation WaitingParent
296− return Nothing
297−
298−-- If a new thread cannot be created then the computation is run in the same
299−-- thread, but the functional behavior remains the same.
300−
301−async :: MonadAsync m => AsyncT m a -> AsyncT m a
302−async action = AsyncT $ do
303− resumeContextWith False action
304− spawningParentDone
305−
306−-- | Makes an asyncly action from a callback setter function; can be used to
307−-- convert existing callback style code into asyncly style code. The first
308−-- parameter is a callback setter function. When we are called back,
309−-- 'makeAsync' behaves as if it was an async computation that just returned a
310−-- value of type 'a'. After the computation is done the result of the action
311−-- in second parameter is returned to the callback.
312−--
313−makeAsync :: MonadAsync m => ((a -> m ()) -> m ()) -> AsyncT m a
314−makeAsync cbsetter = AsyncT $ do
315− -- XXX should we force fork a thread or keep running the context of the
316− -- callback?
317− ctx <- get
318− lift $ cbsetter $ \a -> do
319− let s = resumeContextWith False (return a)
320− _ <- runStateT s ctx
321− return ()
322− spawningParentDone
323−
324−-- scatter
325−each :: (MonadIO m, MonadBaseControl IO m, MonadThrow m)
326− => [a] -> AsyncT m a
327−each xs = foldl (<|>) empty $ map (async . return) xs
328−
329−------------------------------------------------------------------------------
330−-- Controlling thread quota
331−------------------------------------------------------------------------------
332−
333−-- XXX Should n be Word32 instead?
334−-- | Runs a computation under a given thread limit. A limit of 0 means new
335−-- tasks start synchronously in the current thread. New threads are created by
336−-- 'parallel', and APIs that use parallel.
337−threads :: MonadIO m => Int -> AsyncT m a -> AsyncT m a
338−threads n process = AsyncT $ do
339− oldCr <- gets threadCredit
340− newCr <- liftIO $ newIORef n
341− modify $ \s -> s { threadCredit = newCr }
342− r <- runAsyncT $ process
343− <** (AsyncT $ do
344− modify $ \s -> s { threadCredit = oldCr }
345− return (Just ())
346− ) -- restore old credit
347− return r
348−
349−{-
350−-- This can be used to set, increase or decrease the existing limit. The limit
351−-- is shared by multiple threads and therefore needs to modified atomically.
352−-- Note that when there is no limit the limit is set to maxBound it can
353−-- overflow with an increment and get reduced instead of increasing.
354−-- XXX should we use a Maybe instead? Or use separate inc/dec/set APIs to
355−-- handle overflow properly?
356−--
357−-- modifyThreads :: MonadIO m => (Int -> Int) -> AsyncT m ()
358−-- modifyThreads f =
359−-}
360−
36130------------------------------------------------------------------------------
36231-- Running the monad
36332------------------------------------------------------------------------------
36433
36534-- | Run an 'AsyncT m' computation and collect the results generated by each
36635-- thread of the computation in a list.
367−waitAsync :: (MonadIO m, MonadCatch m)
36+waitAsync :: (MonadAsync m, MonadCatch m)
36837 => (a -> AsyncT m a) -> Maybe (IORef [Log]) -> AsyncT m a -> m ()
36938waitAsync finalizer lref m = do
37039 childChan <- liftIO $ atomically newTChan
@@ -389,12 +58,12 @@ waitAsync finalizer lref m = do
38958
39059-- | Invoked to store the result of the computation in the context and finish
39160-- the computation when the computation is done
392−gatherResult :: MonadIO m => IORef [a] -> a -> AsyncT m a
61+gatherResult :: MonadAsync m => IORef [a] -> a -> AsyncT m a
39362gatherResult ref r = do
39463 liftIO $ atomicModifyIORef ref $ \rs -> (r : rs, ())
39564 mzero
39665
397−gather :: (MonadIO m, MonadCatch m) => AsyncT m a -> AsyncT m [a]
66+gather :: (MonadAsync m, MonadCatch m) => AsyncT m a -> AsyncT m [a]
39867gather m = AsyncT $ do
39968 resultsRef <- liftIO $ newIORef []
40069 lift $ waitAsync (gatherResult resultsRef) Nothing m
@@ -403,7 +72,7 @@ gather m = AsyncT $ do
40372
40473-- | Run an 'AsyncT m' computation and collect the results generated by each
40574-- thread of the computation in a list.
406−wait :: (MonadIO m, MonadCatch m) => AsyncT m a -> m [a]
75+wait :: (MonadAsync m, MonadCatch m) => AsyncT m a -> m [a]
40776wait m = do
40877 resultsRef <- liftIO $ newIORef []
40978 waitAsync (gatherResult resultsRef) Nothing m
@@ -411,14 +80,14 @@ wait m = do
41180
41281-- | Run an 'AsyncT m' computation, wait for it to finish and discard the
41382-- results.
414−wait_ :: (MonadIO m, MonadCatch m) => AsyncT m a -> m ()
83+wait_ :: (MonadAsync m, MonadCatch m) => AsyncT m a -> m ()
41584wait_ m = waitAsync (const mzero) Nothing m
41685
41786------------------------------------------------------------------------------
41887-- Logging
41988------------------------------------------------------------------------------
42089
421−logged :: (Loggable a, MonadIO m) => AsyncT m a -> AsyncT m a
90+logged :: (Loggable a, MonadAsync m) => AsyncT m a -> AsyncT m a
42291logged m = AsyncT $ do
42392 ctx <- get
42493 case journal ctx of
@@ -467,7 +136,7 @@ logged m = AsyncT $ do
467136-- completed threads output is returned and for suspended threads the logs to
468137-- resume them are returned.
469138
470−suspend :: MonadIO m => AsyncT m ()
139+suspend :: MonadAsync m => AsyncT m ()
471140suspend = logged $ AsyncT $ do
472141 ctx <- get
473142 case logsRef ctx of
@@ -492,13 +161,12 @@ withLog m (Log entries) = AsyncT $ do
492161
493162-- | Compose a computation using previously captured list of logs. Composes a
494163-- group of computations, one for each log.
495−eachWithLog :: (MonadIO m, MonadBaseControl IO m, MonadThrow m)
496− => AsyncT m a -> [Log] -> AsyncT m a
164+eachWithLog :: MonadAsync m => AsyncT m a -> [Log] -> AsyncT m a
497165eachWithLog m logs = each logs >>= withLog m
498166
499167-- | Run an 'AsyncT m' computation with logging enabled and collect the results
500168-- or logs generated by each thread of the computation.
501−waitLogged :: (MonadIO m, MonadCatch m) => AsyncT m a -> m ([a], [Log])
169+waitLogged :: (MonadAsync m, MonadCatch m) => AsyncT m a -> m ([a], [Log])
502170waitLogged m = do
503171 resultsRef <- liftIO $ newIORef []
504172 lref <- liftIO $ newIORef []
@@ -510,7 +178,7 @@ waitLogged m = do
510178-- | Run an 'AsyncT m' computation with logging enabled, wait for it to finish
511179-- and discard the results. If the computation suspends collect the logs to
512180-- replay later.
513−waitLogged_ :: (MonadIO m, MonadCatch m) => AsyncT m a -> m [Log]
181+waitLogged_ :: (MonadAsync m, MonadCatch m) => AsyncT m a -> m [Log]
514182waitLogged_ m = do
515183 lref <- liftIO $ newIORef []
516184 waitAsync (const mzero) (Just lref) m
test/3-async.hs+2−2
@@ -11,10 +11,10 @@ main = do
1111 liftIO $ hSetBuffering stdout LineBuffering
1212 mainThread <- liftIO myThreadId
1313 liftIO $ putStrLn $ "Main thread: " ++ show mainThread
14− x <- async $ liftIO (randomIO :: IO Int)
14+ x <- liftIO (randomIO :: IO Int)
1515 liftIO $ putStrLn $ show x
1616
17− y <- async $ liftIO (randomIO :: IO Int)
17+ y <- liftIO (randomIO :: IO Int)
1818
1919 liftIO $ threadDelay 1000000
2020 evThread <- liftIO myThreadId
test/3-sample.hs+1−1
@@ -33,7 +33,7 @@ main = wait_ $ threads 0 $ do
3333sample :: (Eq a, MonadIO m, MonadBaseControl IO m, MonadThrow m)
3434 => IO a -> Int -> AsyncT m a
3535sample action interval = do
36− v <- async $ liftIO action
36+ v <- liftIO action
3737 prev <- liftIO $ newIORef v
3838 async (return v) <|> loop action prev
3939 where loop act prev = loop'
test/Main.hs+69−26
@@ -1,11 +1,11 @@
11module Main (main) where
22
3−import Test.Hspec
4−
3+import Asyncly
54import Control.Applicative ((<|>))
5+import Control.Concurrent (myThreadId, threadDelay)
66import Control.Monad.IO.Class (liftIO)
7−import Asyncly
8−import Data.List (sort)
7+import Data.List (nub, sort)
8+import Test.Hspec
99
1010default (Int)
1111
@@ -14,50 +14,93 @@ main = hspec $ do
1414 it "Captures a return value using wait" $
1515 wait (return 0) `shouldReturn` ([0] :: [Int])
1616 it "Alternative composition of sync tasks" $
17− wait (return 0 <|> return 1) `shouldReturn` ([0] :: [Int])
18− it "Creates a single async thread" $
19− wait (async $ return 0) `shouldReturn` ([0] :: [Int])
20− it "async ignoring return value" $
21− wait (async (return 0 :: AsyncT IO Int) >> return 1)
22− `shouldReturn` ([1] :: [Int])
17+ ((wait $ threads 0 $ (return 0 <|> return 1)) >>= return . sort) `shouldReturn` ([0, 1] :: [Int])
2318 it "Alternative composition of async and sync tasks" $
24− (wait (async (return 0) <|> return 1) >>= return . sort)
19+ ((wait (threads 0 ((async (return 0) <|> return 1)))) >>= return . sort)
2520 `shouldReturn` ([0,1] :: [Int])
2621 it "Alternative composition of async tasks" $
2722 (wait (async (return 0) <|> async (return 1)) >>= return . sort)
2823 `shouldReturn` ([0,1] :: [Int])
29− it "Nested async tasks" $
30− (wait $ (async $ async $ return 0)) `shouldReturn` ([0] :: [Int])
31− it "Nested async tasks ignoring return values" $
32− (wait $ (async $ async $ return 0 :: AsyncT IO Int) >> return 1)
33− `shouldReturn` ([1] :: [Int])
3424 it "Nested async tasks with Alternative" $
35− (wait (async (async $ return 0) <|> return 1) >>= return . sort)
36− `shouldReturn` ([0,1] :: [Int])
25+ (wait (async (async $ return 0 <|> return 1) <|> return 1) >>= return . sort)
26+ `shouldReturn` ([0,1,1] :: [Int])
3727 it "General example" $
3828 (wait generalExample >>= return . sort)
39− `shouldReturn` ([7,7,8,8,8,8,8,8,9,9,9,9,9,9,10,10] :: [Int])
29+ `shouldReturn` ([8,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,13] :: [Int])
4030 it "General example synchronous" $
4131 (wait (threads 0 generalExample) >>= return . sort)
42− `shouldReturn` ([7,7,8,8,8,8,8,8,9,9,9,9,9,9,10,10] :: [Int])
32+ `shouldReturn` ([8,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,13] :: [Int])
33+ it "Alternative forks threads - 1" $
34+ ((wait $ (liftIO myThreadId) <|> (liftIO myThreadId))
35+ >>= (\x -> print x >> return x)
36+ >>= return . \x -> (length x, length (nub x)))
37+ `shouldReturn` ((2, 2) :: (Int, Int))
38+ it "Alternative forks threads - 2" $
39+ ((wait $
40+ (liftIO myThreadId)
41+ <|> (liftIO myThreadId)
42+ <|> (liftIO myThreadId))
43+ >>= (\x -> print x >> return x)
44+ >>= return . \x -> (length x, length (nub x)))
45+ `shouldReturn` ((3, 3) :: (Int, Int))
46+ it "Alternative does not fork when using threads 0" $
47+ ((wait $ threads 0 $ (liftIO myThreadId) <|> (liftIO myThreadId))
48+ >>= (\x -> print x >> return x)
49+ >>= return . \x -> (length x, length (nub x)))
50+ `shouldReturn` ((2, 1) :: (Int, Int))
51+ it "async primitive forces a fork at the end of Alternative\
52+ \ (undesired but that's how the implementation behaves)" $
53+ ((wait $ threads 0 $ (liftIO myThreadId) <|> async (liftIO myThreadId))
54+ >>= (\x -> print x >> return x)
55+ >>= return . \x -> (length x, length (nub x)))
56+ `shouldReturn` ((2, 2) :: (Int, Int))
57+ it "async primitive forces a fork at the beginning" $
58+ ((wait $ threads 0 $ async (liftIO myThreadId) <|> (liftIO myThreadId))
59+ >>= (\x -> print x >> return x)
60+ >>= return . \x -> (length x, length (nub x)))
61+ `shouldReturn` ((2, 2) :: (Int, Int))
62+ it "async primitive forces a fork in the middle" $
63+ ((wait $ threads 0 $
64+ (liftIO myThreadId)
65+ <|> async (liftIO (threadDelay 1000000) >> liftIO myThreadId)
66+ <|> (liftIO myThreadId))
67+ >>= (\x -> print x >> return x)
68+ >>= return . \x -> (length x, length (nub x)))
69+ `shouldReturn` ((3, 2) :: (Int, Int))
70+ it "async primitive forces a fork - 3" $
71+ ((wait $ threads 0 $
72+ (liftIO myThreadId)
73+ <|> async (liftIO (threadDelay 1000000) >> liftIO myThreadId)
74+ <|> async (liftIO (threadDelay 1000000) >> liftIO myThreadId))
75+ >>= (\x -> print x >> return x)
76+ >>= return . \x -> (length x, length (nub x)))
77+ `shouldReturn` ((3, 3) :: (Int, Int))
78+ it "async primitive forces a fork - 4" $
79+ ((wait $ threads 0 $
80+ async (liftIO (threadDelay 1000000) >> liftIO myThreadId)
81+ <|> async (liftIO (threadDelay 1000000) >> liftIO myThreadId)
82+ <|> async (liftIO (threadDelay 1000000) >> liftIO myThreadId))
83+ >>= (\x -> print x >> return x)
84+ >>= return . \x -> (length x, length (nub x)))
85+ `shouldReturn` ((3, 3) :: (Int, Int))
4386
4487generalExample :: AsyncT IO Int
4588generalExample = do
4689 liftIO $ return ()
47− liftIO $ print "hello"
48− x <- async (return 1)
49− y <- async (return 2)
90+ liftIO $ putStr ""
91+ x <- return 1
92+ y <- return 2
5093 z <- do
5194 x1 <- async (return 1) <|> async (return 2)
5295 liftIO $ return ()
53− liftIO $ print "hello"
96+ liftIO $ putStr ""
5497 y1 <- (return 1) <|> async (return 2)
5598 z1 <- do
5699 x11 <- async (return 1) <|> (return 2)
57100 y11 <- async (return 1) <|> async (return 2)
58101 z11 <- async (return 1) <|> (return 2)
59102 liftIO $ return ()
60− liftIO $ print "hello"
61− return (x11 + y11)
103+ liftIO $ putStr ""
104+ return (x11 + y11 + z11)
62105 return (x1 + y1 + z1)
63106 return (x + y + z)
test/alternative.hs+3−3
@@ -6,7 +6,7 @@ import Control.Applicative
66
77import Asyncly
88
9−main = wait_ $ threads 3 $ do
9+main = wait_ $ threads 1 $ do
1010 liftIO $ hSetBuffering stdout LineBuffering
1111 mainThread <- liftIO myThreadId
1212 liftIO $ putStrLn $ "Main thread: " ++ show mainThread
@@ -14,14 +14,14 @@ main = wait_ $ threads 3 $ do
1414 eventA <|> eventB
1515
1616eventA = do
17− x <- async $ liftIO (randomIO :: IO Int)
17+ x <- liftIO (randomIO :: IO Int)
1818 evThread <- liftIO myThreadId
1919 liftIO $ putStrLn $ "X Event thread: " ++ show evThread
2020 liftIO $ putStrLn $ "x = " ++ (show x)
2121 return x
2222
2323eventB = do
24− y <- async $ liftIO (randomIO :: IO Int)
24+ y <- liftIO (randomIO :: IO Int)
2525
2626 -- liftIO $ threadDelay 10000000
2727 evThread <- liftIO myThreadId