-- | | Implements locks which can be locked "globally" or "locally". -- A global lock prevents any other lock; a local lock allows other local -- locks. -- -- There are some subtle decisions to be made about when to give preference -- to local, and when to global, locks. There are two important cases: -- (1) When we free a global lock, and there is another queued global lock, -- we take that global lock (or the earliest for which someone is -- waiting, if there's a choice), irrespective of whether anyone is -- waiting for a local lock. -- (2) When at least one local lock is held, we allow people to acquire -- further local locks, even if there are queued global locks. -- -- A bad consequence of (2) is that a global lock can be indefinitely not -- satisfied by a carefully-timed sequence of finite local locks: -- -- local locks : --- --- --- --- . . . -- --- --- --- . . . -- no global lock can be acquired at all. -- -- However the alternative, of not permitting any fresh local locks when -- a global lock is queued, is worse (in my opinion), since if a thread -- attempts to acquire two local locks, one inside the other, and another -- attempts to acquire a global lock, the whole thing can deadlock. -- -- Thread 1 : acquire local lock attempt to acquire second local lock => DEADLOCK. -- Thread 2 : wait for global lock -- -- We could deal with this partially by allowing local locks for free -- to a thread which already holds one, but this is more complicated and -- I suspect theoretically dodgy. -- -- A consequence of this decision is that threads should avoid creating -- automated repeated sequences of local locks on the same VSem. module Util.VSem( VSem, newVSem, synchronizeLocal, synchronizeGlobal, acquireLocal, -- :: VSem -> IO () releaseLocal, -- :: VSem -> IO () ) where import Control.Concurrent import Control.Exception import Util.Computation import Util.Queue data VSemState = VSemState { queuedGlobals :: Queue (MVar ()), queuedLocals :: [MVar ()], nLocalLocks :: Int -- ^ -1 if the vSem is globally locked, otherwise the number of local -- locks. } -- | A lock which can be globally or locally locked. -- At any time, a @VSem@ is either globally locked once, or locally locked -- zero or more times. Global locks always take priority over local locks. newtype VSem = VSem (MVar VSemState) -- | Creates a 'VSem'. newVSem :: IO VSem newVSem = do mVar <- newMVar (VSemState { queuedGlobals = emptyQ, queuedLocals = [], nLocalLocks = 0 }) return (VSem mVar) -- | Perform an action while locking a 'VSem' locally. synchronizeLocal :: VSem -> IO b -> IO b synchronizeLocal vSem act = do acquireLocal vSem finally act (releaseLocal vSem) -- | Perform an action while locking a 'VSem' globally. synchronizeGlobal :: VSem -> IO b -> IO b synchronizeGlobal vSem act = do acquireGlobal vSem finally act (releaseGlobal vSem) vSemAct :: VSem -> (VSemState -> IO (VSemState,b)) -> IO b vSemAct (VSem mVar) update = modifyMVar mVar update -- | Acquire a local lock on a 'VSem' acquireLocal :: VSem -> IO () acquireLocal vSem = do act <- vSemAct vSem (\ vSemState -> if nLocalLocks vSemState <0 then do mVar <- newEmptyMVar return (vSemState { queuedLocals = mVar : queuedLocals vSemState}, takeMVar mVar ) else return (vSemState { nLocalLocks = nLocalLocks vSemState + 1}, done) ) act -- | Release a local lock on a 'VSem' releaseLocal :: VSem -> IO () releaseLocal vSem = vSemAct vSem (\ vSemState -> do let nLocalLocks0 = nLocalLocks vSemState nLocalLocks1 = nLocalLocks0 - 1 case (nLocalLocks1,removeQ (queuedGlobals vSemState)) of (0,Just (mVar,queuedGlobals1)) -> do putMVar mVar () return (vSemState {nLocalLocks = -1, queuedGlobals = queuedGlobals1 },()) _ -> return (vSemState {nLocalLocks = nLocalLocks1},()) ) -- | Acquire a global lock on a 'VSem' acquireGlobal :: VSem -> IO () acquireGlobal vSem = do act <- vSemAct vSem (\ vSemState -> do let nLocalLocks0 = nLocalLocks vSemState if nLocalLocks0 == 0 then return (vSemState {nLocalLocks = -1},done) else do mVar <- newEmptyMVar return (vSemState { queuedGlobals = insertQ (queuedGlobals vSemState) mVar}, takeMVar mVar ) ) act -- | Release a global lock on a 'VSem' releaseGlobal :: VSem -> IO () releaseGlobal vSem = vSemAct vSem (\ vSemState -> case (removeQ (queuedGlobals vSemState),queuedLocals vSemState) of (Just (mVar,queuedGlobals1),_) -> do putMVar mVar () return (vSemState {queuedGlobals = queuedGlobals1},()) (Nothing,queuedLocals0) -> do mapM_ (\ mVar -> putMVar mVar ()) queuedLocals0 return (vSemState {queuedLocals = [], nLocalLocks = length queuedLocals0},()) )