lvish-1.0: Data/Concurrent/SNZI.hs
-- | A Scalable Non-Zero Indicator
--
-- A SNZI is a kind of concurrent counter which can be incremented, decremented,
-- and queried for equality with 0. The interface is a bit more complex,
-- though: it is exposed as N values (where N = the number of CPUs), each
-- providing an @arrive@ and @depart@ operation, together with a single polling
-- action querying the value of the counter. The client MUST NOT invoke
-- @depart@ more times than @arrive@ on any single value.
--
-- The implementation is based on http://dl.acm.org/citation.cfm?id=1281106, but
-- significantly simplified by allowing a call to @arrive@ to block indefinitely
-- until other such calls complete. (Thus the algorithm is no longer
-- non-blocking in theory; its liveness depends on assuming that the OS-level
-- thread scheduler is fair.)
--
-- The basic design is to have a *tree* of counters; each child node in the tree
-- is allowed to invoke @arrive@/@depart@ on its parents. There are two invariants:
--
-- * The number of @depart@s (decrements) must never outnumber the @arrive@s
-- (increments) at any point in the tree. This invariant is partially
-- dependent on the client, which must ensure it for the exposed leaf
-- counters.
--
-- * The number of @arrive@s a child has invoked on a parent can outnumber the
-- @depart@s iff the total number of arrives at the child outnumbers the departs
-- at the child.
--
-- The idea is that child nodes act as "filters": they only need to invoke
-- @arrive@/@depart@ on their parents when their own value changes from 0 to 1 or 1
-- to 0 (i.e., when they change to/from having a surplus).
--
-- To maintain the above invariants, however, child nodes use a special
-- representation: if n >= 0, it represents the counter, but if n = -1 the child
-- is "locked". The locked value is needed to handle races between @arrive@s
-- when the node is currently at 0. The thread that wins the race will move the
-- counter from 0 to -1, thereby effectively "locking" it. Subsequently, it
-- will invoke @arrive@ on the parent, and then finally "unlock" the counter by
-- setting it to the value 1. See the paper for details on why a protocol like
-- this is needed (the paper uses a more complex, lock-free protocol). Such a
-- protocol is *not* needed for @depart@, however.
module Data.Concurrent.SNZI
where
import System.IO.Unsafe
import Control.Reagent
import Control.Monad
import GHC.Conc
import Data.IORef
import Data.Atomics
import Data.Concurrent.AlignedIORef
-- | An entry point for a shared SNZI value
data SNZI =
Child (AlignedIORef Int) SNZI
| Root (AlignedIORef Int)
-- | Signal the presence of a thread at a SNZI
arrive :: SNZI -> IO ()
arrive (Root cnt) = react $ atomicUpdate_ (ref cnt) (+1)
arrive (Child cnt parent) =
let upd 0 = Just (-1, True)
upd (-1) = Nothing
upd n = Just (n+1, False)
in do
tellParent <- react $ atomicUpdate (ref cnt) upd
when tellParent $ do
arrive parent
writeBarrier
writeIORef (ref cnt) 1
data TellParent = Yes | No | Err
-- | Signal the departure of a thread at a SNZI. IMPORTANT: depart MUST NOT be
-- called more times than arrive for a given SNZI value.
depart :: SNZI -> IO ()
depart (Root cnt) = react $ atomicUpdate_ (ref cnt) (\x -> x-1)
depart (Child cnt parent) =
let upd 0 = Just (0, Err)
upd (-1) = Nothing
upd 1 = Just (0, Yes)
upd n = Just (n-1, No)
in do
tellParent <- react $ atomicUpdate (ref cnt) upd
case tellParent of
No -> return ()
Yes -> depart parent
Err -> do putStrLn "SNZI BUG: departs outnumber arrives"
error "SNZI BUG: departs outnumber arrives"
-- Helper function to generate a tree of SNZI values.
makeTree :: Int -> [SNZI] -> [SNZI] -> IO [SNZI]
makeTree n parents children =
if n >= numCapabilities then return children
else case parents of
[] -> makeTree 0 children []
(parent:parents') -> do
c1 <- newAlignedIORef 0
c2 <- newAlignedIORef 0
makeTree (n+2) parents' $ Child c1 parent : Child c2 parent : children
-- | Create a shared SNZI values with numCapabilities number of entry points,
-- together with a polling action that returns "true" when no threads are
-- present.
newSNZI :: IO ([SNZI], IO Bool)
newSNZI = do
rootRef <- newAlignedIORef 0
leaves <- makeTree 1 [] [Root rootRef]
return (leaves, readIORef (ref rootRef) >>= return . (== 0))