我正在实现一个在内存中保存一些数据的网络应用程序。有些请求会读取此数据以进行处理,有些请求会更新此数据。
在这种情况下,多个读者可以同时对数据进行操作,但编写者需要独占访问内存中的数据。我想实现一个reader-writer lock来解决这个问题。我还希望锁定服务员的公平性属性以FIFO顺序处理,以避免读写饥饿。
Haskell标准库似乎没有提供这样的功能。我发现concurrency-extra
提供了这个功能,但是库看起来没有维护(并且在LTS 3.22之后被从堆栈中删除) - 而且它的公平性属性对我来说并不清楚。
我觉得有点令人惊讶的是,标准的haskell库和堆栈中没有读写器锁库 - 不是许多软件中常见的读写器模式吗?或者是否有一种完全不同的(也许是无锁定的)方法在Haskell中是首选的?
编辑:更确切地说,在公平属性上,当编写器被阻塞等待获取锁时,只有在编写器获取并释放写锁后才应允许后续的读锁请求 - 类似于MVar
公平财产 - MVar
s have a FIFO property
答案 0 :(得分:3)
读写器锁定很容易在STM上实现。
data LockState = Writing | Reading Int
type Lock = TVar LockState
startReading :: Lock -> STM ()
startReading lock = do
s <- readTVar lock
case s of
Writing -> retry
Reading n -> writeTVar (Reading (succ n))
stopReading :: Lock -> STM ()
stopReading lock = do
s <- readTVar lock
case s of
Writing -> error "stopReading: lock in writing state?!"
Reading n -> writeTVar (Reading (pred n))
startWriting :: Lock -> STM ()
startWriting lock = do
s <- readTVar lock
case s of
Reading 0 -> writeTVar Writing
_ -> retry
stopWriting :: Lock -> STM ()
stopWriting lock = do
s <- readTVar lock
case s of
Writing -> writeTVar lock (Reading 0)
_ -> error "stopwriting: lock in non-writing state?!"
我对以上内容的主要担忧是:1)对我来说看起来有点矫枉过正,2)我们仍无法保证STM的公平性(活跃度)。
我想可以在MVar
之上实现一个类似的库,虽然这会更复杂,特别是如果我们想要保证公平。
我很想避免使用MVar
并使用信号量,而是使用QSem
来保证FIFO语义。使用这些,可以用Dijkstra风格实现读者/作者。
答案 1 :(得分:1)
确实concurrent-extra
doesn't provide fairness。
正如 chi 所写,STM
无法保证公平。但我们可以使用IO
在STM
中进行操作。我的想法是将其他状态添加到chi LockState
,表明读者无法获得锁定:
data LockState = Writing | Reading Int | Waiting
然后编写者应首先将状态设置为Waiting
,然后等待所有读者释放锁定。请注意,等待应该在单独的STM
交易中执行,这就是为什么我们无法保证STM
的公平性。
Here是一个示例实现:它不在Hackage上,但你可以提供它(是BSD许可的。)
优化实施以最小化唤醒。对于单TVar
,当锁处于Waiting
状态时,每个读取器释放不必要的唤醒所有等待获取锁的读者。所以我有两个TVar
个,一个用于锁定状态,另一个用于读者数量。
ADDED:Here是我与IRC用户 Cale 就读写锁实现的陷阱进行的一次有趣(而且相当长)的讨论。
答案 2 :(得分:1)
最佳解决方案取决于读者/作者关系,但我认为您只能使用MVar
解决问题。
让
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
带输出
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
读者1,2和3同时运行,当4 WRITER working begin
下一个请求等待它时,1,2和3可以终止。
(进入FIFO的stdout输出和流程订单在这个例子中不准确,但是已经处理或结算的项目数显示实际订单)