1{-# LANGUAGE ConstraintKinds #-}
2{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE RecordWildCards #-}
6-- Module : Asyncly.Threads
7-- Copyright : (c) 2017 Harendra Kumar
10-- Maintainer : harendra.kumar@gmail.com
11-- Stability : experimental
31import Control.Concurrent (ThreadId, forkIO, killThread,
33import Control.Concurrent.STM (TChan, atomically, newTChan,
34 readTChan, tryReadTChan,
36import Control.Exception (fromException,
38import qualified Control.Exception.Lifted as EL
39import Control.Monad (when)
40import Control.Monad.Catch (MonadThrow, throwM)
41import Control.Monad.State (MonadIO (..),
42 StateT (..), runStateT,
44import Control.Monad.Trans.Class (MonadTrans (lift))
45import Control.Monad.Trans.Control (MonadBaseControl, liftBaseWith)
46import Data.Atomics (atomicModifyIORefCAS)
47import Data.IORef (IORef, modifyIORef, newIORef,
48 readIORef, writeIORef)
49import Data.List (delete)
50import Data.Maybe (isJust)
52import Unsafe.Coerce (unsafeCoerce)
54import Control.Monad.Trans.Recorder (Paused (..), Recording)
56type MonadAsync m = (MonadIO m, MonadBaseControl IO m, MonadThrow m)
58------------------------------------------------------------------------------
59-- Parent child thread communication types
60------------------------------------------------------------------------------
62data ChildEvent = ChildDone ThreadId (Maybe SomeException)
64------------------------------------------------------------------------------
65-- State threaded around the monad
66------------------------------------------------------------------------------
70 ---------------------------------------------------------------------------
72 ---------------------------------------------------------------------------
74 -- When we suspend we save the logs in this IORef and exit.
75 logsRef :: Maybe (IORef [Recording])
76 -- XXX this functionality can be in a separate layer?
77 , location :: Location
79 ---------------------------------------------------------------------------
80 -- Thread creation, communication and cleanup
81 ---------------------------------------------------------------------------
82 -- When a new thread is created the parent records it in the
83 -- 'pendingThreads' field and 'threadCredit' is decremented. When a child
84 -- thread is done it sends a done event to the parent on an unbounded
85 -- channel and goes away. Before starting a new thread the parent always
86 -- processes the unbounded channel to clear up the pending zombies. This
87 -- strategy ensures that the zombie queue will never grow more than the
88 -- number of running threads. Before exiting, the parent thread waits on
89 -- the channel until all its children are cleaned up.
90 ---------------------------------------------------------------------------
92 -- XXX setup a cleanup computation to run rather than passing all these
95 , childChannel :: TChan ChildEvent
96 -- ^ A channel for the immediate children to communicate to us when they
97 -- die. Each thread has its own dedicated channel for its children
99 -- We always track the child threads, otherwise the programmer needs to
100 -- worry about if some threads may remain hanging because they are stuck in
101 -- an infinite loop or a forever blocked IO. Also, it is not clear whether
102 -- there is a significant gain by disabling tracking. Instead of using
103 -- threadIds we can also keep a count instead, that will allow us to wait
104 -- for all children to drain but we cannot kill them.
106 -- We need these as IORefs since we need any changes to these to be
107 -- reflected back in the calling code that waits for the children.
109 , pendingThreads :: IORef [ThreadId]
110 -- ^ Active immediate child threads spawned by this thread, modified only
111 -- by this thread, therefore atomic modification is not needed.
113 -- By default this limit is shared by the current tree of threads. But at
114 -- any point the 'threads' primitive can be used to start a new limit for
115 -- the computation under that primitive.
117 -- Shared across many threads, therefore atomic access is required.
118 -- This is incremented by the child thread itelf before it exits.
120 , threadCredit :: IORef Int
121 -- ^ How many more threads are allowed to be created?
128 -> Maybe (IORef [Recording])
130initContext childChan pending credit lref =
131 Context { logsRef = lref
133 , childChannel = childChan
134 , pendingThreads = pending
135 , threadCredit = credit }
137------------------------------------------------------------------------------
138-- Where is the computation running?
139------------------------------------------------------------------------------
141-- RemoteNode is used in the Alternative instance. We stop the computation when
142-- the location is RemoteNode, we do not execute any alternatives.
144data Location = Worker | WaitingParent | RemoteNode
147getLocation :: Monad m => StateT Context m Location
148getLocation = gets location
150setLocation :: Monad m => Location -> StateT Context m ()
151setLocation loc = modify $ \Context { .. } -> Context { location = loc, .. }
154------------------------------------------------------------------------------
155-- Pick up from where we left in the previous thread
156------------------------------------------------------------------------------
158{-# SPECIALIZE continue :: [Any] -> a -> StateT Context IO (Maybe a) #-}
159continue :: Monad m => [Any] -> a -> StateT Context m (Maybe a)
164 modify $ \ctx -> ctx {continuations = []}
167continue (f:fs) x = do
168 modify $ \ctx -> ctx {continuations = fs}
169 y <- (unsafeCoerce f) x
172 Just z -> continue fs z
174{-# SPECIALIZE runContext :: Context -> StateT Context IO (Maybe a) -> StateT Context IO () #-}
175runContext :: MonadAsync m => Context -> StateT Context m (Maybe a) -> StateT Context m ()
176runContext ctx action = do
177 _ <- lift $ runStateT m ctx
183 let conts = continuations ctx
186 Just y -> continue conts y >> return ()
189------------------------------------------------------------------------------
190-- Thread Management (creation, reaping and killing)
191------------------------------------------------------------------------------
193-- XXX We are using unbounded channels so this will not block on writing to
194-- pchan. We can use bounded channels to throttle the creation of threads based
195-- on consumption rate.
196processOneEvent :: MonadIO m
199 -> m ([ThreadId], Maybe SomeException)
200processOneEvent (ChildDone tid e) pending = do
201 when (isJust e) $ liftIO $ mapM_ killThread pending
202 return (delete tid pending, Nothing)
204drainChildren :: MonadIO m
207 -> m ([ThreadId], Maybe SomeException)
208drainChildren cchan pending =
210 [] -> return (pending, Nothing)
212 ev <- liftIO $ atomically $ readTChan cchan
213 (p, e) <- processOneEvent ev pending
214 maybe (drainChildren cchan p) (const $ return (p, e)) e
216waitForChildren :: MonadIO m => Context -> m (Maybe SomeException)
217waitForChildren ctx = do
218 let pendingRef = pendingThreads ctx
219 pending <- liftIO $ readIORef pendingRef
220 (p, e) <- drainChildren (childChannel ctx) pending
221 liftIO $ writeIORef pendingRef p
224tryReclaimZombies :: (MonadIO m, MonadThrow m) => Context -> m ()
225tryReclaimZombies ctx = do
226 let cchan = childChannel ctx
227 pendingRef = pendingThreads ctx
229 pending <- liftIO $ readIORef pendingRef
233 mev <- liftIO $ atomically $ tryReadTChan cchan
237 (p, e) <- processOneEvent ev pending
238 liftIO $ writeIORef pendingRef p
239 maybe (return ()) throwM e
240 tryReclaimZombies ctx
242{-# NOINLINE waitForOneEvent #-}
243waitForOneEvent :: (MonadIO m, MonadThrow m) => Context -> m ()
244waitForOneEvent ctx = do
245 -- XXX assert pending must have at least one element
246 -- assert that the tid is found in our list
247 let cchan = childChannel ctx
248 pendingRef = pendingThreads ctx
250 ev <- liftIO $ atomically $ readTChan cchan
251 pending <- liftIO $ readIORef pendingRef
252 (p, e) <- processOneEvent ev pending
253 liftIO $ writeIORef pendingRef p
254 maybe (return ()) throwM e
256-- XXX this is not a real semaphore as it does not really block on wait,
257-- instead it returns whether the value is zero or non-zero.
259{-# INLINE waitQSemB #-}
260waitQSemB :: IORef Int -> IO Bool
261waitQSemB sem = atomicModifyIORefCAS sem $ \n ->
266signalQSemB :: IORef Int -> IO ()
267signalQSemB sem = atomicModifyIORefCAS sem $ \n -> (n + 1, ())
269-- Allocation of threads
271-- global thread limit
272-- thread fan-out i.e. per thread children limit
273-- min per thread allocation to avoid starvation
275-- dynamic adjustment based on the cost, speed of consumption, cpu utilization
276-- etc. We need to adjust the limits based on cost, throughput and latencies.
278-- The event producer thread must put the work on a work-queue and the child
279-- threads can pick it up from there. But if there is just one consumer then it
280-- may not make sense to have a separate producer unless the producing cost is
284{-# INLINE handleResult #-}
285handleResult :: MonadIO m
286 => Context -> Either SomeException a -> m (Maybe SomeException)
287handleResult ctx res =
290 case fromException e of
291 Just (Paused recording) ->
292 maybe (passException e)
293 (handleRecording recording) (logsRef ctx)
294 Nothing -> passException e
295 Right _ -> waitForChildren ctx
299 handleRecording recording ref = do
300 liftIO $ atomicModifyIORefCAS ref $ \logs -> (recording : logs, ())
304 liftIO $ readIORef (pendingThreads ctx) >>= mapM_ killThread
307forkFinally1 :: MonadAsync m
309 -> StateT Context m (Maybe a)
310 -> (Either SomeException () -> IO ())
311 -> StateT Context m ThreadId
312forkFinally1 ctx action preExit =
313 EL.mask $ \restore ->
314 liftBaseWith $ \runInIO -> forkIO $ do
315 _ <- runInIO $ EL.try (restore (runContext ctx action))
317 -- XXX restore state here
320-- | Run a given context in a new thread.
322forkContext :: (MonadBaseControl IO m, MonadIO m, MonadThrow m)
323 => StateT Context m (Maybe a) -> StateT Context m ()
324forkContext action = do
326 childCtx <- childContext parentCtx
327 tid <- forkFinally1 childCtx action
328 (beforeExit childCtx (childChannel parentCtx))
329 updatePendingThreads parentCtx tid
333 updatePendingThreads :: (MonadIO m, MonadThrow m)
334 => Context -> ThreadId -> m ()
335 updatePendingThreads ctx tid = do
336 -- update the new thread before reclaiming zombies so that if it exited
337 -- already reclaim finds it in the list and does not panic.
338 liftIO $ modifyIORef (pendingThreads ctx) $ (\ts -> tid:ts)
339 tryReclaimZombies ctx
341 childContext ctx = do
342 pendingRef <- liftIO $ newIORef []
343 chan <- liftIO $ atomically newTChan
344 -- shares the threadCredit of the parent by default
346 { pendingThreads = pendingRef
347 , childChannel = chan
350 beforeExit ctx pchan res = do
351 r <- handleResult ctx res
352 signalQSemB (threadCredit ctx)
354 liftIO $ atomically $ writeTChan pchan (ChildDone tid r)
356-- | Decide whether to resume the context in the same thread or a new thread
358canFork :: Context -> IO Bool
360 gotCredit <- waitQSemB (threadCredit context)
363 pending <- readIORef $ pendingThreads context
367 -- XXX If we have unreclaimable child threads e.g.
368 -- infinite loop, this is going to deadlock us. We need
369 -- special handling for those cases. Add those to
370 -- unreclaimable list? And always execute them in an
371 -- async thread, cannot use sync for those.
373 waitForOneEvent context
377-- Housekeeping, invoked after spawning of all child tasks is done and the
378-- parent task needs to terminate. Either the task is fully done or we handed
379-- it over to another thread, in any case the current thread is done.
381spawningParentDone :: MonadIO m => StateT Context m (Maybe a)
382spawningParentDone = do
384 when (loc /= RemoteNode) $ setLocation WaitingParent
387-- | The task may be run in the same thread or in a new thread depending on the
388-- forceAsync parameter and the current thread quota.
390{-# SPECIALIZE runAsyncTask :: Bool -> StateT Context IO (Maybe a)
391 -> StateT Context IO (Maybe a) #-}
392{-# INLINE runAsyncTask #-}
393runAsyncTask :: MonadAsync m
395 -> StateT Context m (Maybe a)
396 -> StateT Context m (Maybe a)
397runAsyncTask forceAsync action = do
399 then forkContext action
402 can <- liftIO $ canFork context
404 False -> runContext context action
405 True -> forkContext action
408-- XXX After setting the callback we should wait for the callback otherwise we
409-- will just go away. We should use the same finalization mechanism that we use
412-- | Makes an asyncly action from a callback setter function; can be used to
413-- convert existing callback style code into asyncly style code. The first
414-- parameter is a callback setter function. When we are called back,
415-- 'makeAsync' behaves as if it was an async computation that just returned a
416-- value of type 'a'. After the computation is done the result of the action
417-- in second parameter is returned to the callback.
419makeCont :: MonadAsync m => ((a -> m ()) -> m ()) -> StateT Context m (Maybe a)
420makeCont cbsetter = do
421 -- XXX should we force fork a thread or keep running in the context of the
424 lift $ cbsetter $ \a -> do
425 let s = runAsyncTask False (return (Just a))
431------------------------------------------------------------------------------
432-- Controlling thread quota
433------------------------------------------------------------------------------
435-- XXX Should n be Word32 instead?
436-- | Runs a computation under a given thread limit. A limit of 0 means all new
437-- tasks start synchronously in the current thread unless overridden by
439threadCtl :: MonadAsync m
441 -> StateT Context m a
442 -> StateT Context m a
443threadCtl n action = do
444 oldCr <- gets threadCredit
445 newCr <- liftIO $ newIORef n
446 modify $ \s -> s { threadCredit = newCr }
448 modify $ \s -> s { threadCredit = oldCr }
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list