diff options
Diffstat (limited to 'Control/Concurrent')
| -rw-r--r-- | Control/Concurrent/MSampleVar.hs | 164 | 
1 files changed, 164 insertions, 0 deletions
| diff --git a/Control/Concurrent/MSampleVar.hs b/Control/Concurrent/MSampleVar.hs new file mode 100644 index 0000000..578d391 --- /dev/null +++ b/Control/Concurrent/MSampleVar.hs @@ -0,0 +1,164 @@ +{-# LANGUAGE DeriveDataTypeable, CPP #-} +-- +-- Module      :  Control.Concurrent.MSampleVar +-- Copyright   :  (c) Chris Kuklewicz 2011 +-- License     :  3 clause BSD-style (see the file LICENSE) +--  +-- Maintainer  :  haskell@list.mightyreason.com +-- Stability   :  experimental +-- Portability :  non-portable (concurrency) +-- + +-- | 'MSampleVar' is a safer version of the "Control.Concurrent.SampleVar" in +-- base.  The same problem as QSem(N) is being fixed, that of handling waiters +-- that die before being woken normally.  For "Control.Concurrent.SampleVar" in +-- base this error can lead to thinking a full 'SampleVar' is really empty and +-- cause 'writeSampleVar' to hang.  The 'MSampleVar' in this module is immune +-- to this error, and has a simpler implementation. +-- +module Control.Concurrent.MSampleVar +       ( -- * Sample Variables +         MSampleVar, +         newEmptySV, -- :: IO (MSampleVar a) +         newSV,      -- :: a -> IO (MSampleVar a) +         emptySV,    -- :: MSampleVar a -> IO () +         readSV,     -- :: MSampleVar a -> IO a +         writeSV,    -- :: MSampleVar a -> a -> IO () +         isEmptySV,  -- :: MSampleVar a -> IO Bool +       ) where + +import Control.Monad(void,join) +import Control.Concurrent.MVar(MVar,newMVar,newEmptyMVar,tryTakeMVar,takeMVar,putMVar,withMVar,isEmptyMVar) +import Control.Exception(mask_) +import Data.Typeable + +-- | +-- Sample variables are slightly different from a normal 'MVar': +--  +--  * Reading an empty 'MSampleVar' causes the reader to block. +--    (same as 'takeMVar' on empty 'MVar') +--  +--  * Reading a filled 'MSampleVar' empties it and returns value. +--    (same as 'takeMVar') +-- +--  * Try reading a filled 'MSampleVar' returns a Maybe value. +--    (same as 'tryTakeMVar') +--  +--  * Writing to an empty 'MSampleVar' fills it with a value, and +--    potentially, wakes up a blocked reader (same as for 'putMVar' on +--    empty 'MVar'). +-- +--  * Writing to a filled 'MSampleVar' overwrites the current value. +--    (different from 'putMVar' on full 'MVar'.) +-- +-- The readers queue in FIFO order, with the lead reader joining the writers in +-- a second FIFO queue to access the stored value.  Thus writers can jump the +-- queue of non-leading waiting readers to update the value, but the lead +-- reader has to wait on all previous writes to finish before taking the value. +-- +-- This design choice emphasises that each reader sees the most up-to-date +-- value possible while still guaranteeing progress. +data MSampleVar a = MSampleVar { readQueue :: MVar () +                               , lockedStore :: MVar (MVar a) } +  deriving ( Eq +#if __GLASGOW_HASKELL__ >= 707 +           , Typeable +#endif +           ) + +#if __GLASGOW_HASKELL__ < 707 +instance Typeable1 MSampleVar where +  typeOf1 _ = mkTyConApp tc [] +    where tc = mkTyCon "MSampleVar" +#endif + + +-- | 'newEmptySV' allocates a new MSampleVar in an empty state.  No futher +-- allocation is done when using the 'MSampleVar'. +newEmptySV :: IO (MSampleVar a) +newEmptySV = do +  newReadQueue <- newMVar () +  newLockedStore <- newMVar =<< newEmptyMVar +  return (MSampleVar { readQueue = newReadQueue +                     , lockedStore = newLockedStore }) + +-- | 'newSV' allocates a new MSampleVar containing the passed value.  The value +-- is not evalated or forced, but stored lazily.  No futher allocation is done +-- when using the 'MSampleVar'. +newSV :: a -> IO (MSampleVar a) +newSV a = do +  newReadQueue <- newMVar () +  newLockedStore <- newMVar =<< newMVar a +  return (MSampleVar { readQueue = newReadQueue +                     , lockedStore = newLockedStore }) + +-- | 'isEmptySV' can block and be interrupted, in which case it does nothing. +-- If 'isEmptySV' returns then it reports the momentary status the +-- 'MSampleVar'.  Using this value without producing unwanted race conditions +-- is left up to the programmer. +isEmptySV :: MSampleVar a -> IO Bool +isEmptySV (MSampleVar _ ls) = withMVar ls isEmptyMVar +  -- (withMVar ls) might block, interrupting is okay + +-- | If the 'MSampleVar' is full, forget the value and leave it empty. +-- Otherwise, do nothing.  This avoids any the FIFO queue of blocked 'readSV' +-- threads. +-- +-- 'emptySV' can block and be interrupted, in which case it does nothing.  If +-- 'emptySV' returns then it left the 'MSampleVar' in an empty state. +emptySV :: MSampleVar a -> IO () +emptySV (MSampleVar _ ls) = withMVar ls (void . tryTakeMVar) +  -- (withMVar ls) might block, interrupting is okay + +-- | Wait for a value to become available, then take it and return.  The queue +-- of blocked 'readSV' threads is a fair FIFO queue. +-- +-- 'readSV' can block and be interrupted, in which case it takes nothing.  If +-- 'readSV returns normally then it has taken a value. +readSV :: MSampleVar a -> IO a +readSV (MSampleVar rq ls) =  mask_ $ withMVar rq $ \ () -> +  join $ withMVar ls (return . takeMVar) +  -- (withMVar rq) might block, interrupting is okay +  -- (withMVar ls) might block, interrupting is okay +  -- join (takeMVar _) will block if empty, interrupting is okay + +-- | Write a value into the 'MSampleVar', overwriting any previous value that +-- was there. +-- +-- 'writeSV' can block and be interrupted, in which case it does nothing. +writeSV :: MSampleVar a -> a -> IO () +writeSV (MSampleVar _ ls) a = mask_ $ withMVar ls $ \ v -> do +  void (tryTakeMVar v) +  putMVar v a  -- cannot block +  -- (withMVar ls) might block, interrupting is okay + +{- + Design notes: + + 1) The outer MVar of lockedStore is employed in 'writeSV'.  If two 'writeSV' are + racing in different threads then without the "withMVar ls" they can each + execute "void (tryTakeMVar v)" and then both execute "putMVar v a", causing + the second to block.  Change putMVar to tryPutMVar lets the first 'writeSV' + win which arguably contradicts the specification, though this race makes it a + weak contradiction. + + Thus the lockedStore outer MVar is used as a FIFO queue for writeSV/emptySV + that gives the "previous" in the specification a precise meaning. + + 2) There is no 'tryReadSV' because the desired semantics are unclear. With + 'tryTakeMVar' one is guaranteed to block and a value (Just a) if and only if + 'takeMVar' would have suceeded without blocking. Also, if you know there are + no other readers then a Nothing return from 'tryTakeMVar' means that it is + empty, which is the handiest property. + + 3) An alternate design would queue the writers separately and let only + lead-reader and lead-writer access the stored value.  Imagine several queued + writers and no readers are waiting and then a reader arrives, this reader can + see a value from the middle of the queue of writers.  This would no longer + guarantees the most up-to-date value is read. + + The current design has a very orderly priority of readers and writers.  Design + (3) makes the ordering between readers and writers choatic.  Design (1) goes + further and also makes ordering between different writers chaotic. + +-} | 
