-- Simple racey semaphores without quantity and without starvation prevention
--
-- Author: Patrick Maier
-------------------------------------------------------------------------------

module Control.Parallel.HdpH.Internal.Data.Sem 
  ( -- * semaphore type
    Sem,     -- no instances

    -- * basic operations
    new,     -- :: IO Sem    
    wait,    -- :: Sem -> IO ()
    signal,  -- :: Sem -> IO ()

    -- * convenience
    signalPeriodically  -- :: Sem -> Int -> IO ()
  ) where

import Prelude hiding (error)
import Control.Concurrent (threadDelay)
import Control.Concurrent.MVar (MVar, newEmptyMVar, takeMVar, putMVar)
import Control.Monad (join)
import Data.Functor ((<$>))
import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef)


-------------------------------------------------------------------------------
-- A simple semaphore just maintains a list of threads being blocked on it,
-- and provides operations for waiting and signalling (blocking and wakeing
-- up one thread per operation, respectively).

newtype Sem = Sem (IORef [MVar ()])


-- Creates a new semaphore.
new :: IO Sem
new = Sem <$> newIORef []


-- Blocks calling thread, waiting for a 'signal' on the given semaphore.
-- Note that there can be races between 'wait' and 'signal' in that 'signal'
-- can find no blocked threads (and thus do nothing) just before another
-- thread is about to 'wait'. This effect is due to the semaphore not
-- actually guarding a quantity.
wait :: Sem -> IO ()
wait (Sem sem) = do
  b <- newEmptyMVar
  atomicModifyIORef sem $ \ blocked -> (b:blocked, ())
  takeMVar b


-- Wakes up one blocked thread (actually, the last thread to block) if there
-- are any blocked threads.
signal :: Sem -> IO ()
signal (Sem sem) =
  join $
    atomicModifyIORef sem $ \ blocked ->
      case blocked of
        []        -> ([],      return ())
        b:blocked -> (blocked, putMVar b ())


-- Nonterminating action periodically, every 'interval' microseconds,
-- signalling the given semaphore. This is one way to deal with the races
-- that may arise between 'wait' and 'signal'.
signalPeriodically :: Sem -> Int -> IO ()
signalPeriodically sem interval = do
  threadDelay interval
  signal sem
  signalPeriodically sem interval