haskellconcurrencylockingreaderwriterlock

Reader-Writer Lock in Haskell


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 MVars fairness property - MVars have a FIFO property


Solution

  • 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)