2014-12-20 14:01:51 +03:00
|
|
|
{-# LANGUAGE RankNTypes #-}
|
|
|
|
|
|
|
|
-- | A runner for concurrent monads to systematically detect
|
|
|
|
-- concurrency errors such as data races and deadlocks.
|
|
|
|
--
|
|
|
|
-- As an example, consider this program, which has two locks and a
|
|
|
|
-- shared variable. Two threads are spawned, which claim the locks,
|
|
|
|
-- update the shared variable, and release the locks. The main thread
|
|
|
|
-- waits for them both to terminate, and returns the final result.
|
|
|
|
--
|
2014-12-21 12:38:25 +03:00
|
|
|
-- > bad :: ConcCVar (cvar t) (m t) => m t Int
|
2014-12-20 14:01:51 +03:00
|
|
|
-- > bad = do
|
2014-12-21 10:47:45 +03:00
|
|
|
-- > a <- newEmptyCVar
|
|
|
|
-- > b <- newEmptyCVar
|
2014-12-20 14:01:51 +03:00
|
|
|
-- >
|
2014-12-21 12:38:25 +03:00
|
|
|
-- > c <- newCVar 0
|
2014-12-20 14:01:51 +03:00
|
|
|
-- >
|
2014-12-21 12:38:25 +03:00
|
|
|
-- > j1 <- spawn $ lock a >> lock b >> modifyCVar_ c (return . succ) >> unlock b >> unlock a
|
|
|
|
-- > j2 <- spawn $ lock b >> lock a >> modifyCVar_ c (return . pred) >> unlock a >> unlock b
|
2014-12-20 14:01:51 +03:00
|
|
|
-- >
|
2014-12-21 10:47:45 +03:00
|
|
|
-- > takeCVar j1
|
|
|
|
-- > takeCVar j2
|
2014-12-20 14:01:51 +03:00
|
|
|
-- >
|
2014-12-21 10:47:45 +03:00
|
|
|
-- > takeCVar c
|
2014-12-20 14:01:51 +03:00
|
|
|
--
|
|
|
|
-- The correct result is 0, as it starts out as 0 and is incremented
|
|
|
|
-- and decremented by threads 1 and 2, respectively. However, note the
|
|
|
|
-- order of acquisition of the locks in the two threads. If thread 2
|
|
|
|
-- pre-empts thread 1 between the acquisition of the locks (or if
|
|
|
|
-- thread 1 pre-empts thread 2), a deadlock situation will arise, as
|
|
|
|
-- thread 1 will have lock `a` and be waiting on `b`, and thread 2
|
|
|
|
-- will have `b` and be waiting on `a`.
|
|
|
|
|
|
|
|
module Control.Monad.Conc.SCT
|
|
|
|
( -- *Systematic Concurrency Testing
|
|
|
|
SCTScheduler
|
2014-12-21 15:42:43 +03:00
|
|
|
, Decision(..)
|
2014-12-20 14:01:51 +03:00
|
|
|
, runSCT
|
|
|
|
, sctRandom
|
2014-12-21 15:42:43 +03:00
|
|
|
, showTrace
|
2014-12-20 14:01:51 +03:00
|
|
|
) where
|
|
|
|
|
|
|
|
import Control.Monad.Conc.Fixed
|
|
|
|
import System.Random (RandomGen, randomR)
|
|
|
|
|
|
|
|
-- | An @SCTScheduler@ is like a regular 'Scheduler', except it builds
|
|
|
|
-- a log of scheduling decisions made.
|
2014-12-21 15:42:43 +03:00
|
|
|
type SCTScheduler s = Scheduler (s, [Decision])
|
|
|
|
|
|
|
|
-- | Scheduling decisions are based on the state of the running
|
|
|
|
-- program, and so we can capture some of that state in recording what
|
|
|
|
-- specific decision we made.
|
|
|
|
data Decision =
|
|
|
|
Start ThreadId
|
|
|
|
-- ^ Start a new thread, because the last was blocked (or it's the
|
|
|
|
-- initial thread).
|
|
|
|
| Continue
|
|
|
|
-- ^ Continue running the last thread for another step.
|
|
|
|
| SwitchTo ThreadId
|
|
|
|
-- ^ Pre-empt the running thread, and switch to another.
|
|
|
|
deriving (Eq, Show)
|
2014-12-20 14:01:51 +03:00
|
|
|
|
|
|
|
-- | Run a concurrent program under a given scheduler a number of
|
|
|
|
-- times, collecting the results and the scheduling that gave rise to
|
|
|
|
-- them.
|
|
|
|
--
|
|
|
|
-- The initial state for each run is the final state of the last run,
|
|
|
|
-- so it is important that the scheduler actually maintain some
|
|
|
|
-- internal state, or all the results will be identical.
|
2014-12-21 15:42:43 +03:00
|
|
|
runSCT :: SCTScheduler s -> s -> Int -> (forall t. Conc t a) -> IO [(Maybe a, [Decision])]
|
2014-12-20 14:01:51 +03:00
|
|
|
runSCT sched s runs c = runSCT' s runs where
|
|
|
|
runSCT' _ 0 = return []
|
|
|
|
runSCT' s n = do
|
2014-12-21 15:42:43 +03:00
|
|
|
(res, (s', log)) <- runConc' sched (s, [Start 0]) c
|
2014-12-20 14:01:51 +03:00
|
|
|
rest <- runSCT' s' $ n - 1
|
|
|
|
return $ (res, log) : rest
|
|
|
|
|
|
|
|
-- | A simple pre-emptive random scheduler.
|
|
|
|
sctRandom :: RandomGen g => SCTScheduler g
|
2014-12-21 15:42:43 +03:00
|
|
|
sctRandom (g, log) last threads = (tid, (g', log ++ [decision])) where
|
2014-12-20 14:01:51 +03:00
|
|
|
(choice, g') = randomR (0, length threads - 1) g
|
|
|
|
tid = threads !! choice
|
2014-12-21 15:42:43 +03:00
|
|
|
decision | tid == last = Continue
|
|
|
|
| last `elem` threads = SwitchTo tid
|
|
|
|
| otherwise = Start tid
|
|
|
|
|
|
|
|
-- | Pretty-print a scheduler trace.
|
|
|
|
showTrace :: [Decision] -> String
|
|
|
|
showTrace = trace "" 0 where
|
|
|
|
trace log num (Start tid:ds) = thread log num ++ trace ("S" ++ show tid) 1 ds
|
|
|
|
trace log num (Continue:ds) = trace log (num + 1) ds
|
|
|
|
trace log num (SwitchTo tid:ds) = thread log num ++ trace ("P" ++ show tid) 1 ds
|
|
|
|
trace log num [] = thread log num
|
|
|
|
|
|
|
|
thread "" _ = ""
|
|
|
|
thread log num = log ++ replicate num '-'
|