I am implementing a web-app that holds some data in memory. Some requests read this data for processing and some requests update this data.
In this scenario, multiple readers can concurrently operate on the data, but a writer needs exclusive access to the data in memory. I want to implement a reader-writer lock to solve this problem. I also want the fairness property that waiters on the lock are processed in FIFO order to avoid read and write starvation.
The Haskell standard libraries do not appear to provide such functionality. I found concurrency-extra
gives this functionality, but the library seems to be unmaintained (and was removed from stackage after LTS 3.22) - and its fairness properties are not clear to me.
I find it a bit surprising that there is no reader-writer lock library in standard haskell libraries and in stackage - isn't the reader-writer pattern common in many software? Or is there a completely different (perhaps lock-free) approach that is preferred in Haskell?
EDIT: More precisely on the fairness property, when a writer is blocked waiting to acquire the lock, subsequent read-lock requests should be allowed only after the writer acquires and releases the write-lock - similar to MVar
s fairness property - MVar
s have a FIFO property
The best solution depend of readers/writers relation but I think you can solve your problem only using MVar
.
Let
import System.Clock
import Text.Printf
import Control.Monad
import Control.Concurrent
import Control.Concurrent.MVar
t__ :: Int -> String -> IO ()
t__ id msg = do
TimeSpec s n <- getTime Realtime
putStrLn $ printf "%3d.%-3d - %d %s" (s `mod` 1000) n id msg
reader :: MVar [Int] -> Int -> IO ()
reader mv id = do
t__ id $ "reader waiting"
xs <- readMVar mv
t__ id $ "reader working begin"
threadDelay (1 * 10^6)
t__ id $ "reader working ends, " ++ show (length xs) ++ " items"
writer :: MVar [Int] -> Int -> IO ()
writer mv id = do
t__ id $ "WRITER waiting"
xs <- takeMVar mv
t__ id $ "WRITER working begin"
threadDelay (3 * 10^6)
t__ id $ "WRITER working ends, " ++ show (1 + length xs) ++ " items"
putMVar mv (id: xs)
main = do
mv <- newMVar []
forM_ (take 10 $ zipWith (\f id -> forkIO (f mv id)) (cycle [reader, reader, reader, writer]) [1..]) $ \p -> do
threadDelay (10^5)
p
getLine
with output
c:\tmp>mvar.exe +RTS -N20
486.306991300 - 1 reader waiting
486.306991300 - 1 reader working begin
486.416036100 - 2 reader waiting
486.416036100 - 2 reader working begin
486.525191000 - 3 reader waiting
486.525191000 - 3 reader working begin
486.634286500 - 4 WRITER waiting
486.634286500 - 4 WRITER working begin
486.743378400 - 5 reader waiting
486.852406800 - 6 reader waiting
486.961564300 - 7 reader waiting
487.070645900 - 8 WRITER waiting
487.179673900 - 9 reader waiting
487.288845100 - 10 reader waiting
487.320003300 - 1 reader working ends, 0 items
487.429028600 - 2 reader working ends, 0 items
487.538202000 - 3 reader working ends, 0 items
489.642147400 - 10 reader working begin
489.642147400 - 4 WRITER working ends, 1 items
489.642147400 - 5 reader working begin
489.642147400 - 6 reader working begin
489.642147400 - 7 reader working begin
489.642147400 - 8 WRITER working begin
489.642147400 - 9 reader working begin
490.655157400 - 10 reader working ends, 1 items
490.670730800 - 6 reader working ends, 1 items
490.670730800 - 7 reader working ends, 1 items
490.670730800 - 9 reader working ends, 1 items
490.686247400 - 5 reader working ends, 1 items
492.681178800 - 8 WRITER working ends, 2 items
readers 1, 2 and 3 run simultaneously, when 4 WRITER working begin
next requests wait for it but 1, 2 and 3 can terminate.
(the stdout output and process order to enter into FIFO is not accurate on this example but the number of items readed or settled show the real order)