packages feed

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 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