【问题标题】:Reader-Writer Lock in HaskellHaskell 中的读写锁
【发布时间】:2016-09-16 01:28:44
【问题描述】:

我正在实现一个在内存中保存一些数据的网络应用程序。一些请求读取此数据进行处理,一些请求更新此数据。

在这种情况下,多个读取器可以同时对数据进行操作,但写入器需要独占访问内存中的数据。我想实现一个reader-writer lock 来解决这个问题。我还希望按 FIFO 顺序处理锁上的等待者的公平属性,以避免读取和写入饥饿。

Haskell 标准库似乎没有提供这样的功能。我发现 concurrency-extra 提供了此功能,但该库似乎未维护(并在 LTS 3.22 之后从堆栈中删除) - 我不清楚它的公平性。

我发现在标准的 haskell 库和堆栈中没有读写器锁库有点令人惊讶——读写器模式在许多软件中不是很常见吗?或者是否有一种完全不同的(可能是无锁的)方法在 Haskell 中是首选的?

编辑:更准确地说,在公平属性上,当写入器被阻塞等待获取锁时,只有在写入器获取并释放写入锁后才允许后续的读锁请求 -类似于MVars 公平属性 - MVars have a FIFO property

【问题讨论】:

  • 为什么不Control.Concurrent.MVar(使用takeMVar)?
  • 如果多个读取器都必须取一个 MVar,则它们不能同时操作。在我的场景中,多个读者可以处理数据,但作者需要独占访问权限。
  • 我想既然我们现在有STM和TVars,就不再需要一些更简单的锁定形式了。您可以使用TVars 和STM,或者在MVars 之上实现锁定。 (后者看起来不简单)
  • @donatello“在内存中保存一些数据”我假设您的“内存中数据”是不可变的,那么MVar 动作是原子的;错了吗?
  • 共享数据可以通过对网络应用程序的某些类型的请求(例如内存缓存)进行更新。 MVar 的问题是,只有一个线程可以takeMVar,然后直到它有putMVared,没有其他线程可以访问MVar 中的数据。当我希望并发阅读器不相互阻塞时,这不是所需的行为。

标签: haskell concurrency locking readerwriterlock


【解决方案1】:

读写锁很容易在 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 中的公平性(活性)。

我想可以在MVars 之上实现一个类似的库,尽管这会更复杂,特别是如果我们想保证公平。

我很想避免使用MVars,而是使用信号量,而使用QSem,这样可以保证FIFO语义。使用这些,可以实现 Dijkstra 风格的读取器/写入器。

【讨论】:

  • 是的,这不能保证公平,但感谢您的实施。我现在对TVars 不太熟悉了。
【解决方案2】:

最佳解决方案取决于读者/作者关系,但我认为您只能使用 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 可以终止时。

(本例中stdout输出和进入FIFO的处理顺序不准确,但读取或结算的项目数显示真实顺序)

【讨论】:

  • 谢谢!这就是我需要的答案!
【解决方案3】:

确实concurrent-extradoesn't provide fairness

正如 chi 所写,STM 无法保证公平。但是我们可以在IO 中使用STM。这个想法是在 chi 的LockState 中添加其他状态,表明读取器无法获取锁:

data LockState = Writing | Reading Int | Waiting

那么 writer 应该首先将 state 设置为Waiting,然后等待所有的 reader 释放锁。请注意,等待应该在单独的STM 事务中执行,这就是为什么我们不能保证STM 中的公平性。

Here 是一个示例实现:它不在 Hackage 上,但你可以出售它(它是 BSD 许可的。)

该实施经过优化,可最大限度地减少唤醒。使用单个TVar,当锁处于Waiting 状态时,每个读取器不必要的释放都会唤醒所有等待获取锁的读取器。所以我有两个TVars,一个用于锁定状态,另一个用于读者数量。

已添加:Here 是我与 IRC 用户 Cale 就读写锁实现的缺陷进行的一次有趣(而且相当长)的讨论。

【讨论】:

  • 感谢您的回答,但我将使用 MVar 解决方案。
猜你喜欢
  • 2011-01-25
  • 2011-12-04
  • 2010-09-19
  • 2018-03-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多