massiv-scheduler (empty) → 0.1.0.0
raw patch · 11 files changed
+1054/−0 lines, 11 filesdep +QuickCheckdep +atomic-primopsdep +basebuild-type:Customsetup-changed
Dependencies added: QuickCheck, atomic-primops, base, deepseq, doctest, exceptions, hspec, massiv-scheduler, template-haskell, unliftio-core
Files
- LICENSE +30/−0
- README.md +195/−0
- Setup.hs +33/−0
- massiv-scheduler.cabal +65/−0
- src/Control/Massiv/Scheduler.hs +328/−0
- src/Control/Massiv/Scheduler/Computation.hs +86/−0
- src/Control/Massiv/Scheduler/Queue.hs +118/−0
- tests/Control/Massiv/SchedulerSpec.hs +176/−0
- tests/Main.hs +10/−0
- tests/Spec.hs +1/−0
- tests/doctests.hs +12/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Alexey Kuleshevich (c) 2019++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Alexey Kuleshevich nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,195 @@+# massiv-scheduler++Primary focus of this package is to provide work stealing scheduler for the array processing library+[massiv](https://www.stackage.org/package/massiv). But it can be used for any other project that can+benefit from parallelization of computation.++## QuickStart++A few examples in order to get up and running quickly.++### Schedule simple actions++Work scheduling that does some side effecty stuff and discards the results:++```haskell+interleaveFooBar :: IO ()+interleaveFooBar = do+ withScheduler_ (ParN 2) $ \ scheduler -> do+ putStrLn "Scheduling 1st job"+ scheduleWork scheduler (putStr "foo")+ putStrLn "Scheduling 2nd job"+ scheduleWork scheduler (putStr "bar")+ putStrLn "Awaiting for jobs to be executed:"+ putStrLn "\nDone"+```++In the example above two workers will be created to handle the only two jobs that have been+scheduled. Printing with `putStr` is not thread safe, so the output that you would get with above+function is likely be interleaved:++```haskell+λ> interleaveFooBar+Scheduling 1st job+Scheduling 2nd job+Awaiting for jobs to be executed:+foboar+Done+```++Important to note that only when inner action supplied to the `withScheduler_` exits will the+scheduler start executing scheduled jobs.++### Keeping the results of computation++Another common scenario is to schedule some jobs that produce useful results. In the example below+four works will be spawned off. Due to `ParOn` each of the workers will be pinned to a particular+core.++```haskell+scheduleSums :: IO [Int]+scheduleSums =+ withScheduler (ParOn [1..4]) $ \ scheduler -> do+ scheduleWork scheduler $ pure (10 + 1)+ scheduleWork scheduler $ pure (20 + 2)+ scheduleWork scheduler $ pure (30 + 3)+ scheduleWork scheduler $ pure (40 + 4)+ scheduleWork scheduler $ pure (50 + 5)+```++Despite that the fact that sums are computed in parallel, the results of computation will appear in+the same order they've been scheduled:++```haskell+λ> scheduleSums+[11,22,33,44,55]+```++### Exceptions++Whenever any of the scheduled jobs result in an exception, all of the workers will be killed and the+exception will get re-thrown in the scheduling thread:++```haskell+infiniteJobs :: IO ()+infiniteJobs = do+ withScheduler_ (ParN 5) $ \ scheduler -> do+ scheduleWork scheduler $ putStrLn $ repeat 'a'+ scheduleWork scheduler $ putStrLn $ repeat 'b'+ scheduleWork scheduler $ putStrLn $ repeat 'c'+ scheduleWork scheduler $ pure (4 `div` (0 :: Int))+ scheduleWork scheduler $ putStrLn $ repeat 'd'+ putStrLn "\nDone"+```++Note, that if there was no exception, printing would never stop.++```haskell+λ> infiniteJobs+aaaaaaaaabcdd*** Exception: divide by zero+```++A special case is when a thread is killed by an async exception. Whenever that happens than the+exception will be re-thrown in a scheduling thread, but it will be wrapped in a custom+`WorkerAsyncException` exception. If for some reason you need to recover the original async+exception you can use `fromWorkerAsyncException`. See function documentation for an example.+++### Nested jobs++Scheduling actions can themselves schedule actions indefinitely. That of course means that order of+results produced is no longer deterministic, which is to be expected.++```haskell+nestedJobs :: IO ()+nestedJobs = do+ withScheduler_ (ParN 5) $ \ scheduler -> do+ scheduleWork scheduler $ putStr $ replicate 10 'a'+ scheduleWork scheduler $ do+ putStr $ replicate 10 'b'+ scheduleWork scheduler $ do+ putStr $ replicate 10 'c'+ scheduleWork scheduler $ putStr $ replicate 10 'e'+ scheduleWork scheduler $ putStr $ replicate 10 'd'+ scheduleWork scheduler $ putStr $ replicate 10 'f'+ putStrLn "\nDone"+```++The order in which characters appear is important, since it directly relates to the actual order in+which jobs are being scheduled and executed:++* `c`, `d` and `e` characters will always appear after `b`+* `e` will always appear after `c`++```haskell+λ> nestedJobs+abbafbafbafbafbafbafbafbafbaffcdcdcdcdcdcdcdcdcdcdeeeeeeeeee+Done+```++### Nested parallelism++Nothing really prevents you from having a scheduler within a scheduler. Of course, having multiple+schedulers at the same time seems like an unnecessary overhead, which it is, but if you do have a+use case for it, don't make me stop you, it is OK to go that route.++```haskell+nestedSchedulers :: IO ()+nestedSchedulers = do+ withScheduler_ (ParN 2) $ \ outerScheduler -> do+ scheduleWork outerScheduler $ putStr $ replicate 10 'a'+ scheduleWork outerScheduler $ do+ putStr $ replicate 10 'b'+ withScheduler_ (ParN 2) $ \ innerScheduler -> do+ scheduleWork innerScheduler $ do+ putStr $ replicate 10 'c'+ scheduleWork outerScheduler $ putStr $ replicate 10 'e'+ scheduleWork innerScheduler $ putStr $ replicate 10 'd'+ scheduleWork outerScheduler $ putStr $ replicate 10 'f'+ putStrLn "\nDone"+```++Note that the inner scheduler's job schedules a job for the outer scheduler, which is a bit crazy,+but totally safe.++```haskell+λ> nestedSchedulers+aabababababababababbffffffffffcccccccdcdcdcdddededededeeeeee+Done+```++### Single worker schedulers++If we only have one worker, than everything becomes sequential and deterministic. Consider the same+example from before, but with `Seq` computation strategy.++```haskell+nestedSequentialSchedulers :: IO ()+nestedSequentialSchedulers = do+ withScheduler_ Seq $ \ outerScheduler -> do+ scheduleWork outerScheduler $ putStr $ replicate 10 'a'+ scheduleWork outerScheduler $ do+ putStr $ replicate 10 'b'+ withScheduler_ Seq $ \ innerScheduler -> do+ scheduleWork innerScheduler $ do+ putStr $ replicate 10 'c'+ scheduleWork outerScheduler $ putStr $ replicate 10 'e'+ scheduleWork innerScheduler $ putStr $ replicate 10 'd'+ scheduleWork outerScheduler $ putStr $ replicate 10 'f'+ putStrLn "\nDone"+```++No more interleaving, everything is done in the same order each time the function is invoked.++```haskell+λ> nestedSchedulers+aaaaaaaaaabbbbbbbbbbccccccccccddddddddddffffffffffeeeeeeeeee+Done+```++## Avoiding deadlocks++Any sort of concurrency primitives such as mutual exclusion, semaphores, etc. can easily lead to+deadlocks, starvation and other common problems. Try to avoid them and be careful if you do end up+using them.+
+ Setup.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE CPP #-}+{-# OPTIONS_GHC -Wall #-}+module Main (main) where++#ifndef MIN_VERSION_cabal_doctest+#define MIN_VERSION_cabal_doctest(x,y,z) 0+#endif++#if MIN_VERSION_cabal_doctest(1,0,0)++import Distribution.Extra.Doctest ( defaultMainWithDoctests )+main :: IO ()+main = defaultMainWithDoctests "doctests"++#else++#ifdef MIN_VERSION_Cabal+-- If the macro is defined, we have new cabal-install,+-- but for some reason we don't have cabal-doctest in package-db+--+-- Probably we are running cabal sdist, when otherwise using new-build+-- workflow+#warning You are configuring this package without cabal-doctest installed. \+ The doctests test-suite will not work as a result. \+ To fix this, install cabal-doctest before configuring.+#endif++import Distribution.Simple++main :: IO ()+main = defaultMain++#endif
+ massiv-scheduler.cabal view
@@ -0,0 +1,65 @@+name: massiv-scheduler+version: 0.1.0.0+synopsis: Work stealing scheduler for Massiv (Массив) and other parallel applications.+description: A work stealing scheduler that is used by [massiv](https://www.stackage.org/package/massiv) array librarry, but can be useful for any other library or application that fits such model of computation.+homepage: https://github.com/lehins/massiv+license: BSD3+license-file: LICENSE+author: Alexey Kuleshevich+maintainer: alexey@kuleshevi.ch+copyright: 2018-2019 Alexey Kuleshevich+category: Parallelism, Concurrency+build-type: Custom+extra-source-files: README.md+cabal-version: >=1.10++custom-setup+ setup-depends:+ base+ , Cabal+ , cabal-doctest >=1.0.6++library+ hs-source-dirs: src+ exposed-modules: Control.Massiv.Scheduler++ other-modules: Control.Massiv.Scheduler.Computation+ , Control.Massiv.Scheduler.Queue+ build-depends: base >= 4.8 && < 5+ , atomic-primops+ , deepseq+ , exceptions+ , unliftio-core++ default-language: Haskell2010+ ghc-options: -Wall+++test-suite tests+ type: exitcode-stdio-1.0+ hs-source-dirs: tests+ main-is: Main.hs+ other-modules: Spec+ , Control.Massiv.SchedulerSpec+ build-depends: base >= 4.8 && < 5+ , deepseq+ , massiv-scheduler+ , hspec+ , QuickCheck++ default-language: Haskell2010+ ghc-options: -Wall -fno-warn-orphans -threaded -with-rtsopts=-N++test-suite doctests+ type: exitcode-stdio-1.0+ hs-source-dirs: tests+ main-is: doctests.hs+ build-depends: base+ , doctest >=0.15+ , massiv-scheduler+ , template-haskell+ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/lehins/massiv
+ src/Control/Massiv/Scheduler.hs view
@@ -0,0 +1,328 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- |+-- Module : Control.Massiv.Scheduler+-- Copyright : (c) Alexey Kuleshevich 2018-2019+-- License : BSD3+-- Maintainer : Alexey Kuleshevich <lehins@yandex.ru>+-- Stability : experimental+-- Portability : non-portable+--+module Control.Massiv.Scheduler+ ( -- * Scheduler and strategies+ Comp(..)+ , Scheduler(..)+ -- * Initialize Scheduler+ , withScheduler+ , withScheduler_+ -- * Helper functions+ , fromWorkerAsyncException+ , traverseConcurrently+ , traverseConcurrently_+ , traverse_+ ) where++import Control.Concurrent+import Control.Exception+import Control.Massiv.Scheduler.Computation+import Control.Massiv.Scheduler.Queue+import Control.Monad+import Control.Monad.IO.Unlift+import Data.Atomics (atomicModifyIORefCAS, atomicModifyIORefCAS_)+import Data.Foldable as F (foldl')+import Data.IORef++data Jobs m a = Jobs+ { jobsNumWorkers :: {-# UNPACK #-} !Int+ , jobsQueue :: !(JQueue m a)+ , jobsCountRef :: !(IORef Int)+ }+ -- TODO: Make Jobs a sum type with: ?+ -- ParScheduler+ -- SeqScheduler+ -- sSeqResRef :: !(Maybe (IORef [a]))++-- | Main type for scheduling work. See `withScheduler` or `withScheduler_` for the only ways to get+-- access to such data type.+--+-- @since 0.1.0+data Scheduler m a = Scheduler+ { numWorkers :: {-# UNPACK #-} !Int+ -- ^ Get the number of workers.+ , scheduleWork :: m a -> m ()+ -- ^ Schedule an action to be picked up and computed by a worker from a pool.+ }++-- | This is generally a faster way to traverse while ignoring the result rather than using `mapM_`.+--+-- @since 0.1.0+traverse_ :: (Foldable t, Applicative f) => (a -> f ()) -> t a -> f ()+traverse_ f = F.foldl' (\c a -> c *> f a) (pure ())+++-- | Map an action over each element of the `Foldable` @t@ acccording to the supplied computation+-- strategy.+--+-- @since 0.1.0+traverseConcurrently :: (MonadUnliftIO m, Foldable t) => Comp -> (a -> m b) -> t a -> m [b]+traverseConcurrently comp f xs = withScheduler comp $ \s -> traverse_ (scheduleWork s . f) xs++-- | Just like `traverseConcurrently`, but discard the results of computation.+--+-- @since 0.1.0+traverseConcurrently_ :: (MonadUnliftIO m, Foldable t) => Comp -> (a -> m b) -> t a -> m ()+traverseConcurrently_ comp f xs = withScheduler_ comp $ \s -> traverse_ (scheduleWork s . f) xs++scheduleJobs :: MonadIO m => Jobs m a -> m a -> m ()+scheduleJobs = scheduleJobsWith mkJob++-- | Similarly to `scheduleWork`, but ignores the result of computation, thus having less overhead.+--+-- @since 0.1.0+scheduleJobs_ :: MonadIO m => Jobs m a -> m b -> m ()+scheduleJobs_ = scheduleJobsWith (return . Job_ . void)++scheduleJobsWith :: MonadIO m => (m b -> m (Job m a)) -> Jobs m a -> m b -> m ()+scheduleJobsWith mkJob' Jobs {jobsQueue, jobsCountRef, jobsNumWorkers} action = do+ liftIO $ atomicModifyIORefCAS_ jobsCountRef (+ 1)+ job <-+ mkJob' $ do+ res <- action+ res `seq` dropCounterOnZero jobsCountRef $ retireWorkersN jobsQueue jobsNumWorkers+ return res+ pushJQueue jobsQueue job++-- | Helper function to place `Retire` instructions on the job queue.+retireWorkersN :: MonadIO m => JQueue m a -> Int -> m ()+retireWorkersN jobsQueue n = traverse_ (pushJQueue jobsQueue) $ replicate n Retire++-- | Decrease a counter by one and perform an action when it drops down to zero.+dropCounterOnZero :: MonadIO m => IORef Int -> m () -> m ()+dropCounterOnZero counterRef onZero = do+ jc <-+ liftIO $ atomicModifyIORefCAS+ counterRef+ (\ !i' ->+ let !i = i' - 1+ in (i, i))+ when (jc == 0) onZero+++-- | Runs the worker until the job queue is exhausted, at which point it will exhecute the final task+-- of retirement and return+runWorker :: MonadIO m =>+ JQueue m a+ -> m () -- ^ Action to run upon retirement+ -> m ()+runWorker jQueue onRetire = go+ where+ go =+ popJQueue jQueue >>= \case+ Just job -> job >> go+ Nothing -> onRetire+++-- | Initialize a scheduler and submit jobs that will be computed sequentially or in parallelel,+-- which is determined by the `Comp`utation strategy.+--+-- Here are some cool properties about the `withScheduler`:+--+-- * This function will block until all of the submitted jobs have finished or at least one of them+-- resulted in an exception, which will be re-thrown at the callsite.+--+-- * It is totally fine for nested jobs to submit more jobs for the same or other scheduler+--+-- * It is ok to initialize multiple schedulers at the same time, although that will likely result+-- in suboptimal performance, unless workers are pinned to different capabilities.+--+-- * __Warning__ It is very dangerous to schedule jobs that do blocking `IO`, since it can lead to a+-- deadlock very quickly, if you are not careful. Consider this example. First execution works fine,+-- since there are two scheduled workers, and one can unblock the other, but the second scenario+-- immediately results in a deadlock.+--+-- >>> withScheduler (ParOn [1,2]) $ \s -> newEmptyMVar >>= (\ mv -> scheduleWork s (readMVar mv) >> scheduleWork s (putMVar mv ()))+-- [(),()]+-- >>> import System.Timeout+-- >>> timeout 1000000 $ withScheduler (ParOn [1]) $ \s -> newEmptyMVar >>= (\ mv -> scheduleWork s (readMVar mv) >> scheduleWork s (putMVar mv ()))+-- Nothing+--+-- __Important__: In order to get work done truly in parallel, program needs to be compiled with+-- @-threaded@ GHC flag and executed with @+RTS -N -RTS@ to use all available cores.+--+-- @since 0.1.0+withScheduler ::+ MonadUnliftIO m+ => Comp -- ^ Computation strategy+ -> (Scheduler m a -> m b)+ -- ^ Action that will be scheduling all the work.+ -> m [a]+withScheduler comp = withSchedulerInternal comp scheduleJobs flushResults+++-- | Same as `withScheduler`, but discards results of submitted jobs.+--+-- @since 0.1.0+withScheduler_ ::+ MonadUnliftIO m+ => Comp -- ^ Computation strategy+ -> (Scheduler m a -> m b)+ -- ^ Action that will be scheduling all the work.+ -> m ()+withScheduler_ comp = withSchedulerInternal comp scheduleJobs_ (const (pure ()))+++withSchedulerInternal ::+ MonadUnliftIO m+ => Comp -- ^ Computation strategy+ -> (Jobs m a -> m a -> m ()) -- ^ How to schedule work+ -> (JQueue m a -> m c) -- ^ How to collect results+ -> (Scheduler m a -> m b)+ -- ^ Action that will be scheduling all the work.+ -> m c+withSchedulerInternal comp submitWork collect onScheduler = do+ jobsNumWorkers <-+ case comp of+ Seq -> return 1+ Par -> liftIO getNumCapabilities+ ParOn ws -> return $ length ws+ ParN 0 -> liftIO getNumCapabilities+ ParN n -> return $ fromIntegral n+ sWorkersCounterRef <- liftIO $ newIORef jobsNumWorkers+ jobsQueue <- newJQueue+ jobsCountRef <- liftIO $ newIORef 0+ workDoneMVar <- liftIO newEmptyMVar+ let jobs = Jobs {..}+ scheduler = Scheduler {numWorkers = jobsNumWorkers, scheduleWork = submitWork jobs}+ onRetire = dropCounterOnZero sWorkersCounterRef $ liftIO (putMVar workDoneMVar Nothing)+ -- / Wait for the initial jobs to get scheduled before spawining off the workers, otherwise it would+ -- be trickier to identify the beginning and the end of a job pool.+ _ <- onScheduler scheduler+ -- / Ensure at least something gets scheduled, so retirement can be triggered+ jc <- liftIO $ readIORef jobsCountRef+ when (jc == 0) $ scheduleJobs_ jobs (pure ())+ let spawnWorkersWith fork ws =+ forM ws $ \w ->+ withRunInIO $ \run ->+ mask_ $+ fork w $ \unmask ->+ catch+ (unmask $ run $ runWorker jobsQueue onRetire)+ (unmask . run . handleWorkerException jobsQueue workDoneMVar jobsNumWorkers)+ {-# INLINE spawnWorkersWith #-}+ spawnWorkers =+ case comp of+ Seq -> return []+ -- \ no need to fork threads for a sequential computation+ Par -> spawnWorkersWith forkOnWithUnmask [1 .. jobsNumWorkers]+ ParOn ws -> spawnWorkersWith forkOnWithUnmask ws+ ParN _ -> spawnWorkersWith (\_ -> forkIOWithUnmask) [1 .. jobsNumWorkers]+ {-# INLINE spawnWorkers #-}+ doWork = do+ when (comp == Seq) $ runWorker jobsQueue onRetire+ mExc <- liftIO $ readMVar workDoneMVar+ -- \ wait for all worker to finish. If any one of them had a problem this MVar will+ -- contain an exception+ case mExc of+ Nothing+ -- / Now we are sure all workers have done their job we can safely read all of the+ -- IORefs with results+ -> collect jobsQueue+ Just exc -- / Here we need to unwrap the legit worker exception and rethrow it, so the+ -- main thread will think like it's his own+ | Just (WorkerException wexc) <- fromException exc -> liftIO $ throwIO wexc+ Just exc -> liftIO $ throwIO exc -- Somethig funky is happening, propagate it.+ {-# INLINE doWork #-}+ safeBracketOnError+ spawnWorkers+ (liftIO . traverse_ (`throwTo` WorkerTerminateException))+ (const doWork)+++-- | Specialized exception handler for the work scheduler.+handleWorkerException ::+ MonadIO m => JQueue m a -> MVar (Maybe SomeException) -> Int -> SomeException -> m ()+handleWorkerException jQueue workDoneMVar nWorkers exc =+ case fromException exc of+ Just WorkerTerminateException -> return ()+ -- \ some co-worker died, we can just move on with our death.+ _ ->+ case fromException exc of+ Just asyncExc -> do+ liftIO $ putMVar workDoneMVar $ Just $ toException $ WorkerAsyncException asyncExc+ -- \ Let the main thread know about this terrible async exception.+ -- / Do the co-worker cleanup+ retireWorkersN jQueue (nWorkers - 1)+ liftIO $ throwIO asyncExc+ -- \ do not recover from an async exception+ Nothing -> do+ liftIO $ putMVar workDoneMVar $ Just $ toException $ WorkerException exc+ -- \ Main thread must know how we died+ -- / Do the co-worker cleanup+ retireWorkersN jQueue (nWorkers - 1)+ -- / As to one self, gracefully leave off into the outer world+++-- | This exception should normally be not seen in the wild. The only one that could possibly pop up+-- is the `WorkerAsyncException`.+data WorkerException+ = WorkerException !SomeException+ -- ^ One of workers experienced an exception, main thread will receive the same `SomeException`.+ | WorkerAsyncException !SomeAsyncException+ -- ^ If a worker recieves an async exception, something is utterly wrong. This exception in+ -- particular would mean that one of the workers have died of an asyncronous exception. We don't+ -- want to raise that async exception synchronously in the main thread, therefore it will receive+ -- this wrapper exception instead.+ | WorkerTerminateException+ -- ^ When a brother worker dies of some exception, all the other ones will be terminated+ -- asynchronously with this one.+ deriving Show++instance Exception WorkerException where+ displayException workerExc =+ case workerExc of+ WorkerException exc ->+ "A worker handled a job that ended with exception: " ++ displayException exc+ WorkerAsyncException exc ->+ "A worker was killed with an async exception: " ++ displayException exc+ WorkerTerminateException -> "A worker was terminated by the scheduler"+++-- | If any one of the workers dies with an async exception, it is possible to recover that+-- exception in the main thread with this function. This does not apply to `Seq`uential computation,+-- since no workers are created in such case and async exception will be received by the main thread+-- directly.+--+-- >>> let didAWorkerDie = handleJust fromWorkerAsyncException (return . (== ThreadKilled)) . fmap or+-- >>> :t didAWorkerDie+-- didAWorkerDie :: Foldable t => IO (t Bool) -> IO Bool+-- >>> didAWorkerDie $ withScheduler Par $ \ s -> scheduleWork s $ pure False+-- False+-- >>> didAWorkerDie $ withScheduler Par $ \ s -> scheduleWork s $ myThreadId >>= killThread >> pure False+-- True+-- >>> withScheduler Par $ \ s -> scheduleWork s $ myThreadId >>= killThread >> pure False+-- *** Exception: WorkerAsyncException thread killed+--+-- @since 0.1.0+fromWorkerAsyncException :: Exception e => SomeException -> Maybe e+fromWorkerAsyncException exc =+ case fromException exc of+ Just (WorkerAsyncException asyncExc) -> asyncExceptionFromException (toException asyncExc)+ _ -> Nothing+++-- Copy from unliftio:+safeBracketOnError :: MonadUnliftIO m => m a -> (a -> m b) -> (a -> m c) -> m c+safeBracketOnError before after thing = withRunInIO $ \run -> mask $ \restore -> do+ x <- run before+ res1 <- try $ restore $ run $ thing x+ case res1 of+ Left (e1 :: SomeException) -> do+ -- ignore the exception, see bracket for explanation+ _ :: Either SomeException b <-+ try $ uninterruptibleMask_ $ run $ after x+ throwIO e1+ Right y -> return y
+ src/Control/Massiv/Scheduler/Computation.hs view
@@ -0,0 +1,86 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- |+-- Module : Control.Massiv.Scheduler.Computation+-- Copyright : (c) Alexey Kuleshevich 2018-2019+-- License : BSD3+-- Maintainer : Alexey Kuleshevich <lehins@yandex.ru>+-- Stability : experimental+-- Portability : non-portable+--+module Control.Massiv.Scheduler.Computation+ ( Comp(.., Par)+ ) where++import Control.DeepSeq (NFData(..), deepseq)+#if !MIN_VERSION_base(4,11,0)+import Data.Semigroup+#endif+import Data.Word++-- | Computation strategy to use when scheduling work.+data Comp+ = Seq -- ^ Sequential computation+ | ParOn ![Int]+ -- ^ Schedule workers to run on specific capabilities. Specifying an empty list @`ParOn` []@ or+ -- using `Par` will result in utilization of all available capabilities.+ | ParN {-# UNPACK #-} !Word16+ -- ^ Specify the number of workers that will be handling all the jobs. Difference from `ParOn` is+ -- that workers can jump between cores. Using @`ParN` 0@ will result in using all available+ -- capabilities.+ deriving Eq++-- | Parallel computation using all available cores. Same as @`ParOn` []@+pattern Par :: Comp+pattern Par <- ParOn [] where+ Par = ParOn []++instance Show Comp where+ show Seq = "Seq"+ show Par = "Par"+ show (ParOn ws) = "ParOn " ++ show ws+ show (ParN n) = "ParN " ++ show n+ showsPrec _ Seq = ("Seq" ++)+ showsPrec _ Par = ("Par" ++)+ showsPrec 0 comp = (show comp ++)+ showsPrec _ comp = (("(" ++ show comp ++ ")") ++)++instance NFData Comp where+ rnf comp =+ case comp of+ Seq -> ()+ ParOn wIds -> wIds `deepseq` ()+ ParN n -> n `deepseq` ()+ {-# INLINE rnf #-}++instance Monoid Comp where+ mempty = Seq+ {-# INLINE mempty #-}+ mappend = joinComp+ {-# INLINE mappend #-}++instance Semigroup Comp where+ (<>) = joinComp+ {-# INLINE (<>) #-}++joinComp :: Comp -> Comp -> Comp+joinComp x y =+ case x of+ Seq -> y+ Par -> Par+ ParOn xs ->+ case y of+ Seq -> x+ Par -> Par+ ParOn ys -> ParOn (xs ++ ys)+ ParN 0 -> ParN 0+ ParN n2 -> ParN (max (fromIntegral (length xs)) n2)+ ParN 0 -> ParN 0+ ParN n1 ->+ case y of+ Seq -> x+ Par -> Par+ ParOn ys -> ParN (max n1 (fromIntegral (length ys)))+ ParN n2 -> ParN (max n1 n2)+{-# NOINLINE joinComp #-}
+ src/Control/Massiv/Scheduler/Queue.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE NamedFieldPuns #-}+-- |+-- Module : Control.Massiv.Scheduler.Queue+-- Copyright : (c) Alexey Kuleshevich 2018-2019+-- License : BSD3+-- Maintainer : Alexey Kuleshevich <lehins@yandex.ru>+-- Stability : experimental+-- Portability : non-portable+--+module Control.Massiv.Scheduler.Queue+ ( -- * Queue+ -- ** Pure queue+ Queue+ , emptyQueue+ , pushQueue+ , popQueue+ -- ** Job queue+ , Job(Retire, Job_)+ , mkJob+ , JQueue+ , newJQueue+ , pushJQueue+ , popJQueue+ , flushResults+ -- * Tools+ -- , isDeadlock+ ) where++import Control.Concurrent.MVar+import Control.Monad (join, void)+import Control.Monad.IO.Unlift+import Data.Atomics (atomicModifyIORefCAS)+import Data.IORef+++-- | Pure functional Okasaki queue with total length+data Queue a = Queue { qQueue :: ![a]+ , qStack :: ![a]+ }++emptyQueue :: Queue a+emptyQueue = Queue [] []++pushQueue :: Queue a -> a -> Queue a+pushQueue queue@Queue {qStack} x = queue {qStack = x : qStack}++popQueue :: Queue a -> Maybe (a, Queue a)+popQueue queue@Queue {qQueue, qStack} =+ case qQueue of+ x:xs -> Just (x, queue {qQueue = xs})+ [] ->+ case reverse qStack of+ [] -> Nothing+ y:ys -> Just (y, Queue {qQueue = ys, qStack = []})++data Job m a+ = Job !(IORef a) !(m a)+ | Job_ !(m ())+ | Retire+++mkJob :: MonadIO m => m a -> m (Job m a)+mkJob action = do+ resRef <- liftIO $ newIORef $ error "mkJob: result is uncomputed"+ return $!+ Job resRef $ do+ res <- action+ liftIO $ writeIORef resRef res+ return res+++newtype JQueue m a =+ JQueue (IORef (Queue (Job m a), [IORef a], MVar ()))+++newJQueue :: MonadIO m => m (JQueue m a)+newJQueue = do+ newBaton <- liftIO newEmptyMVar+ queueRef <- liftIO $ newIORef (emptyQueue, [], newBaton)+ return $ JQueue queueRef+++pushJQueue :: MonadIO m => JQueue m a -> Job m a -> m ()+pushJQueue (JQueue jQueueRef) job = do+ newBaton <- liftIO newEmptyMVar+ join $+ liftIO $ atomicModifyIORefCAS+ jQueueRef+ (\(queue, resRefs, baton) ->+ ( ( pushQueue queue job+ , case job of+ Job resRef _ -> resRef : resRefs+ _ -> resRefs+ , newBaton)+ , liftIO $ putMVar baton ()))+++popJQueue :: MonadIO m => JQueue m a -> m (Maybe (m ()))+popJQueue (JQueue jQueueRef) = liftIO inner+ where+ inner =+ join $+ atomicModifyIORefCAS jQueueRef $ \jQueue@(queue, resRefs, baton) ->+ case popQueue queue of+ Nothing -> (jQueue, readMVar baton >> inner)+ Just (job, newQueue) ->+ ( (newQueue, resRefs, baton)+ , case job of+ Job _ action -> return $ Just (void action)+ Job_ action_ -> return $ Just action_+ Retire -> return Nothing)++flushResults :: MonadIO m => JQueue m a -> m [a]+flushResults (JQueue jQueueRef) =+ liftIO $ do+ resRefs <-+ atomicModifyIORefCAS jQueueRef $ \(queue, resRefs, baton) -> ((queue, [], baton), resRefs)+ mapM readIORef $ reverse resRefs
+ tests/Control/Massiv/SchedulerSpec.hs view
@@ -0,0 +1,176 @@+module Control.Massiv.SchedulerSpec (spec) where++import Control.Concurrent (killThread, myThreadId, threadDelay)+import Control.Concurrent.MVar+import Control.DeepSeq+import Control.Exception hiding (assert)+import Control.Exception.Base (ArithException(DivideByZero))+import Control.Massiv.Scheduler+import Data.List (sort)+import Test.Hspec+import Test.QuickCheck+import Test.QuickCheck.Monadic+++instance Arbitrary Comp where+ arbitrary =+ frequency+ [ (20, pure Seq)+ , (10, pure Par)+ , (35, ParOn <$> arbitrary)+ , (35, ParN . getSmall <$> arbitrary)+ ]++prop_SameList :: Comp -> [Int] -> Property+prop_SameList comp xs =+ monadicIO $ run $ do+ xs' <- withScheduler comp $ \scheduler -> mapM_ (scheduleWork scheduler . return) xs+ return (xs === xs')++prop_Recursive :: Comp -> [Int] -> Property+prop_Recursive comp xs =+ monadicIO $+ run $ do+ xs' <- withScheduler comp (schedule xs)+ return (sort xs === sort xs')+ where+ schedule [] _ = return ()+ schedule (y:ys) scheduler = scheduleWork scheduler (schedule ys scheduler >> return y)+++prop_Serially :: Comp -> [Int] -> Property+prop_Serially comp xs =+ monadicIO $+ run $ do+ xs' <- schedule xs+ return (xs === concat xs')+ where+ schedule [] = return []+ schedule (y:ys) = do+ y' <- withScheduler comp (\s -> scheduleWork s (return y))+ ys' <- schedule ys+ return (y':ys')++prop_Nested :: Comp -> [Int] -> Property+prop_Nested comp xs =+ monadicIO $+ run $ do+ xs' <- schedule xs+ return (sort xs === sort (concat xs'))+ where+ schedule [] = return []+ schedule (y:ys) =+ withScheduler comp (\s -> scheduleWork s (schedule ys >>= \ys' -> return (y : concat ys')))++prop_ArbitraryCompNested :: [(Comp, Int)] -> Property+prop_ArbitraryCompNested xs =+ monadicIO $+ run $ do+ xs' <- schedule xs+ return (sort (map snd xs) === sort (concat xs'))+ where+ schedule [] = return []+ schedule ((c, y):ys) =+ withScheduler c (\s -> scheduleWork s (schedule ys >>= \ys' -> return (y : concat ys')))++-- | Ensure proper exception handling.+prop_CatchDivideByZero :: Comp -> Int -> [Positive Int] -> Property+prop_CatchDivideByZero comp k xs =+ assertExceptionIO+ (== DivideByZero)+ (traverseConcurrently+ comp+ (\i -> return (k `div` i))+ (map getPositive xs ++ [0] ++ map getPositive xs))++-- | Ensure proper exception handling.+prop_CatchDivideByZeroNested :: Comp -> Int -> Positive Int -> Property+prop_CatchDivideByZeroNested comp a (Positive k) = assertExceptionIO (== DivideByZero) (schedule k)+ where+ schedule i+ | i < 0 = return []+ | otherwise =+ withScheduler comp (\s -> scheduleWork s (schedule (i - 1) >> return (a `div` i)))+++-- | Make sure one co-worker can kill another one, of course when there are at least two of.+prop_KillBlockedCoworker :: Comp -> Property+prop_KillBlockedCoworker comp =+ assertExceptionIO+ (== DivideByZero)+ (withScheduler_ comp $ \scheduler ->+ if numWorkers scheduler < 2+ then scheduleWork scheduler $ return ((1 :: Int) `div` (0 :: Int))+ else do+ mv <- newEmptyMVar+ scheduleWork scheduler $ readMVar mv+ scheduleWork scheduler $ return ((1 :: Int) `div` (0 :: Int)))++-- | Make sure one co-worker can kill another one, of course when there are at least two of.+prop_KillSleepingCoworker :: Comp -> Property+prop_KillSleepingCoworker comp =+ assertExceptionIO+ (== DivideByZero)+ (withScheduler_ comp $ \scheduler -> do+ scheduleWork scheduler $ return ((1 :: Int) `div` (0 :: Int))+ scheduleWork scheduler $ do+ threadDelay 500000+ error "This should never happen! Thread should have been killed by now.")+++prop_ExpectAsyncException :: Comp -> Property+prop_ExpectAsyncException comp =+ let didAWorkerDie = handleJust fromWorkerAsyncException' (return . (== ThreadKilled)) . fmap or+ fromWorkerAsyncException' =+ case comp of+ Seq -> asyncExceptionFromException+ _ -> fromWorkerAsyncException+ in (monadicIO .+ run .+ didAWorkerDie .+ withScheduler comp $ \s -> scheduleWork s (myThreadId >>= killThread >> pure False)) .&&.+ (monadicIO .+ run . fmap not . didAWorkerDie . withScheduler Par $ \s -> scheduleWork s $ pure False)++spec :: Spec+spec = do+ describe "Seq" $ do+ it "SameList" $ property $ prop_SameList Seq+ it "Recursive" $ property $ prop_Recursive Seq+ it "Nested" $ property $ prop_Nested Seq+ it "Serially" $ property $ prop_Serially Seq+ describe "ParOn" $ do+ it "SameList" $ property $ \ cs -> prop_SameList (ParOn cs)+ it "Recursive" $ property $ \ cs -> prop_Recursive (ParOn cs)+ it "Nested" $ property $ \ cs -> prop_Nested (ParOn cs)+ it "Serially" $ property $ \ cs -> prop_Serially (ParOn cs)+ describe "Arbitrary Comp" $+ it "ArbitraryNested" $ property prop_ArbitraryCompNested+ describe "Exceptions" $ do+ it "CatchDivideByZero" $ property prop_CatchDivideByZero+ it "CatchDivideByZeroNested" $ property prop_CatchDivideByZeroNested+ it "KillBlockedCoworker" $ property prop_KillBlockedCoworker+ it "KillSleepingCoworker" $ property prop_KillSleepingCoworker+ it "ExpectAsyncException" $ property prop_ExpectAsyncException++assertExceptionIO :: (NFData a, Exception exc) =>+ (exc -> Bool) -- ^ Return True if that is the exception that was expected+ -> IO a -- ^ IO Action that should throw an exception+ -> Property+assertExceptionIO isExc action =+ monadicIO $ do+ hasFailed <-+ run+ (catch+ (do res <- action+ res `deepseq` return False) $ \exc ->+ displayException exc `deepseq` return (isExc exc))+ assert hasFailed++++++++
+ tests/Main.hs view
@@ -0,0 +1,10 @@+module Main where++import System.IO (BufferMode(LineBuffering), hSetBuffering, stdout)+import Test.Hspec+import Spec++main :: IO ()+main = do+ hSetBuffering stdout LineBuffering+ hspec spec
+ tests/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --no-main #-}
+ tests/doctests.hs view
@@ -0,0 +1,12 @@+module Main where++import Build_doctests (flags, pkgs, module_sources)+import Data.Foldable (traverse_)+import Test.DocTest (doctest)++main :: IO ()+main = do+ traverse_ putStrLn args+ doctest args+ where+ args = flags ++ pkgs ++ module_sources