1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
|
{-# LANGUAGE CPP
, BangPatterns
, DeriveDataTypeable
, NoImplicitPrelude
#-}
#if __GLASGOW_HASKELL__ >= 704
{-# LANGUAGE Safe #-}
#endif
--------------------------------------------------------------------------------
-- |
-- Module : Control.Concurrent.RLock
-- Copyright : (c) 2010-2011 Bas van Dijk & Roel van Dijk
-- License : BSD3 (see the file LICENSE)
-- Maintainer : Bas van Dijk <v.dijk.bas@gmail.com>
-- , Roel van Dijk <vandijk.roel@gmail.com>
--
-- This module provides the 'RLock' synchronisation mechanism. It was inspired
-- by the Python @RLock@ and Java @ReentrantLock@ objects and should behave in a
-- similar way. See:
--
-- <http://docs.python.org/3.1/library/threading.html#rlock-objects>
--
-- and:
--
-- <http://java.sun.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantLock.html>
--
-- All functions are /exception safe/. Throwing asynchronous exceptions will not
-- compromise the internal state of an 'RLock'.
--
-- This module is intended to be imported qualified. We suggest importing it like:
--
-- @
-- import Control.Concurrent.RLock ( RLock )
-- import qualified Control.Concurrent.RLock as RLock ( ... )
-- @
--
--------------------------------------------------------------------------------
module Control.Concurrent.RLock
( RLock
-- * Creating reentrant locks
, new
, newAcquired
-- * Locking and unlocking
, acquire
, tryAcquire
, release
-- * Convenience functions
, with
, tryWith
, wait
-- * Querying reentrant locks
, State
, state
) where
--------------------------------------------------------------------------------
-- Imports
--------------------------------------------------------------------------------
-- from base:
import Control.Applicative ( liftA2 )
import Control.Concurrent ( ThreadId, myThreadId )
import Control.Concurrent.MVar ( MVar, newMVar, takeMVar, readMVar, putMVar )
import Control.Exception ( bracket_, onException )
import Control.Monad ( return, (>>) )
import Data.Bool ( Bool(False, True), otherwise )
import Data.Eq ( Eq, (==) )
import Data.Function ( ($), (.) )
import Data.Functor ( fmap, (<$>) )
import Data.Maybe ( Maybe(Nothing, Just) )
import Data.List ( (++) )
import Data.Tuple ( fst )
import Data.Typeable ( Typeable )
import Prelude ( Integer, succ, pred, error )
import System.IO ( IO )
#if __GLASGOW_HASKELL__ < 700
import Prelude ( fromInteger )
import Control.Monad ( Monad, fail, (>>=) )
#endif
-- from concurrent-extra (this package):
import Control.Concurrent.Lock ( Lock )
import qualified Control.Concurrent.Lock as Lock
( new, newAcquired, acquire, release, wait )
import Utils ( mask, mask_ )
--------------------------------------------------------------------------------
-- Reentrant locks
--------------------------------------------------------------------------------
{-| A reentrant lock is in one of two states: \"locked\" or \"unlocked\". When
the lock is in the \"locked\" state it has two additional properties:
* Its /owner/: the thread that acquired the lock.
* Its /acquired count/: how many times its owner acquired the lock.
-}
newtype RLock = RLock {un :: MVar (State, Lock)}
deriving (Eq, Typeable)
{-| The state of an 'RLock'.
* 'Nothing' indicates an \"unlocked\" state.
* @'Just' (tid, n)@ indicates a \"locked\" state where the thread identified by
@tid@ acquired the lock @n@ times.
-}
type State = Maybe (ThreadId, Integer)
--------------------------------------------------------------------------------
-- * Creating reentrant locks
--------------------------------------------------------------------------------
-- | Create a reentrant lock in the \"unlocked\" state.
new :: IO RLock
new = do lock <- Lock.new
RLock <$> newMVar (Nothing, lock)
{-|
Create a reentrant lock in the \"locked\" state (with the current thread as
owner and an acquired count of 1).
-}
newAcquired :: IO RLock
newAcquired = do myTID <- myThreadId
lock <- Lock.newAcquired
RLock <$> newMVar (Just (myTID, 1), lock)
--------------------------------------------------------------------------------
-- * Locking and unlocking
--------------------------------------------------------------------------------
{-|
Acquires the 'RLock'. Blocks if another thread has acquired the 'RLock'.
@acquire@ behaves as follows:
* When the state is \"unlocked\", @acquire@ changes the state to \"locked\"
with the current thread as owner and an acquired count of 1.
* When the state is \"locked\" and the current thread owns the lock @acquire@
only increments the acquired count.
* When the state is \"locked\" and the current thread does not own the lock
@acquire@ /blocks/ until the owner releases the lock. If the thread that called
@acquire@ is woken upon release of the lock it will take ownership and change
the state to \"locked\" with an acquired count of 1.
There are two further important properties of @acquire@:
* @acquire@ is single-wakeup. That is, if there are multiple threads blocked on
@acquire@, and the lock is released, only one thread will be woken up. The
runtime guarantees that the woken thread completes its @acquire@ operation.
* When multiple threads are blocked on @acquire@ they are woken up in FIFO
order. This is useful for providing fairness properties of abstractions built
using locks. (Note that this differs from the Python implementation where the
wake-up order is undefined.)
-}
acquire :: RLock -> IO ()
acquire (RLock mv) = do
myTID <- myThreadId
mask_ $ let acq = do t@(mb, lock) <- takeMVar mv
case mb of
Nothing -> do Lock.acquire lock
putMVar mv (Just (myTID, 1), lock)
Just (tid, n)
| myTID == tid -> let !sn = succ n
in putMVar mv (Just (tid, sn), lock)
| otherwise -> do putMVar mv t
Lock.wait lock
acq
in acq
{-|
A non-blocking 'acquire'.
* When the state is \"unlocked\" @tryAcquire@ changes the state to \"locked\"
(with the current thread as owner and an acquired count of 1) and returns
'True'.
* When the state is \"locked\" @tryAcquire@ leaves the state unchanged and
returns 'False'.
-}
tryAcquire :: RLock -> IO Bool
tryAcquire (RLock mv) = do
myTID <- myThreadId
mask_ $ do
t@(mb, lock) <- takeMVar mv
case mb of
Nothing -> do Lock.acquire lock
putMVar mv (Just (myTID, 1), lock)
return True
Just (tid, n)
| myTID == tid -> do let !sn = succ n
putMVar mv (Just (tid, sn), lock)
return True
| otherwise -> do putMVar mv t
return False
{-| @release@ decrements the acquired count. When a lock is released with an
acquired count of 1 its state is changed to \"unlocked\".
Note that it is both an error to release a lock in the \"unlocked\" state and to
release a lock that is not owned by the current thread.
If there are any threads blocked on 'acquire' the thread that first called
@acquire@ will be woken up.
-}
release :: RLock -> IO ()
release (RLock mv) = do
myTID <- myThreadId
mask_ $ do
t@(mb, lock) <- takeMVar mv
let err msg = do putMVar mv t
error $ "Control.Concurrent.RLock.release: " ++ msg
case mb of
Nothing -> err "Can't release an unacquired RLock!"
Just (tid, n)
| myTID == tid -> if n == 1
then do Lock.release lock
putMVar mv (Nothing, lock)
else let !pn = pred n
in putMVar mv (Just (tid, pn), lock)
| otherwise -> err "Calling thread does not own the RLock!"
--------------------------------------------------------------------------------
-- * Convenience functions
--------------------------------------------------------------------------------
{-| A convenience function which first acquires the lock and then
performs the computation. When the computation terminates, whether
normally or by raising an exception, the lock is released.
Note that: @with = 'liftA2' 'bracket_' 'acquire' 'release'@.
-}
with :: RLock -> IO a -> IO a
with = liftA2 bracket_ acquire release
{-|
A non-blocking 'with'. @tryWith@ is a convenience function which first tries to
acquire the lock. If that fails, 'Nothing' is returned. If it succeeds, the
computation is performed. When the computation terminates, whether normally or
by raising an exception, the lock is released and 'Just' the result of the
computation is returned.
-}
tryWith :: RLock -> IO a -> IO (Maybe a)
tryWith l a = mask $ \restore -> do
acquired <- tryAcquire l
if acquired
then do r <- restore a `onException` release l
release l
return $ Just r
else return Nothing
{-|
* When the state is \"locked\" @wait@ /blocks/ until a call to 'release' in
another thread changes it to \"unlocked\".
* When the state is \"unlocked\" @wait@ returns immediately.
@wait@ does not alter the state of the lock.
Note that @wait@ is just a convenience function defined as:
@wait l = 'block' '$' 'acquire' l '>>' 'release' l@
-}
wait :: RLock -> IO ()
wait l = mask_ $ acquire l >> release l
--------------------------------------------------------------------------------
-- * Querying reentrant locks
--------------------------------------------------------------------------------
{-|
Determine the state of the reentrant lock.
Note that this is only a snapshot of the state. By the time a program reacts on
its result it may already be out of date.
-}
state :: RLock -> IO State
state = fmap fst . readMVar . un
|