summaryrefslogtreecommitdiffstats
path: root/Control/Concurrent/MSampleVar.hs
blob: 578d3917e5e1f47c2f34c3eadffdcd404c5e1cae (plain)
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
{-# 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.

-}