如何在Haskell中创建临界区?

4
我有多个“线程”,它们使用forkIO运行。我需要处理互相访问的问题,典型的关键部分/锁定,因为它们将共享公共的Map:一些线程将修改它,另一些线程将读取它。在Haskell中,使用forkIO创建线程的关键部分是什么:API、模块、库?
注:所有这些都在Scotty下工作。

1
@DanielWagner,当Map不需要与其他共享状态保持同步时,atomicModifyIORef'是另一个选择。 - dfeuer
1
@Paul-AG 通常使用纯惰性记忆化字典树技巧时,存在一个几个周期的窗口,在这个窗口内,两个线程都尝试使用相同的键可能会做额外的工作。这是一个非常小的窗口(大约需要在内存中检查单个标记词并立即写入它的时间长度)--特别是考虑到必须由两个线程同时想要相同的键才能达到这个窗口。我建议首先尝试它,因为它在概念上非常简单,并且消费者几乎不需要编写任何代码,而且在等待性能问题之前甚至不必检查您有多少次命中这个几个周期的窗口。 - Daniel Wagner
1
@Paul-AG,那你用错了。atomicModifyIORef mref $ \m -> case lookup k m of Nothing -> (insert k v m, Nothing); r -> (m, r) - dfeuer
1
@Paul-AG 例如,这个包,或者也可以浏览这个搜索结果 - Daniel Wagner
1
唯一不起作用的情况是如果您需要IO来填补缺失的键,这种情况下最好的选择可能是使用MVarmodifyMVar - dfeuer
显示剩余17条评论
2个回答

5

在Haskell中处理并发任务的首选库是stm。与锁定不同,对共享内存(例如共享的Map)的操作可以在原子事务中进行。


我理解的是,如果有2个线程尝试访问共享对象,STM会回滚事务,这意味着我将在内存中拥有2个Map对象:一个用于回滚,另一个用于尝试修改。 - RandomB
1
@Paul-AG 是的,没错。确实,既然我们在这里谈论的是短暂的情况,那么几乎肯定会有两个以上:原始版本,一个线程的修改版本和另一个线程的修改版本。当然,所有通常的懒惰和共享考虑都适用;而且几乎可以肯定,这三个对象所占用的内存显然比任何单个对象所占用的内存少得多。 - Daniel Wagner
STM看起来不错,但我不确定性能如何:我将有很多请求,所以...也许简单的锁定机制会更快。 - RandomB
没有什么可以阻止你在stm之上设置锁。你可以自由定义任何同步策略。 - Li-yao Xia
3
@Li-yaoXia 当有多个共享变量需要保持同步时,STM才显得好用。否则,你会因为没有必要而付出性能代价。如果只是一个缓存,那么使用IORef(或者可能是MVar)更合适。 - dfeuer

5
一个 MVar 是一个可变的变量,它是自己的临界区。
takeMVar :: MVar a -> IO a 
-- Return the contents of the MVar. If the MVar is currently empty, takeMVar will
-- wait until it is full. After a takeMVar, the MVar is left empty.

putMVar :: MVar a -> a -> IO ()
-- Put a value into an MVar. If the MVar is currently full, putMVar will
-- wait until it becomes empty.

MVar YourMapType 的 takeMVar-update-putMVar 基本上就是你所需要的,除非有特定要求需要在更新期间 map 仍然可用。在这种情况下,你可以使用 MVar () 作为互斥锁来控制对任何想要访问的内容的访问。


1
通常最好使用modifyMVar而不是手动获取和放置。它会处理(有些棘手的)异常安全问题。 - dfeuer
@dfeuer 文档中说:“如果没有其他生产者为此MVar,则此函数仅为原子性”。我有点困惑。这到底是什么意思? - n. m.
1
这意味着一个进程可以使用modifyMVar获取MVar,而在第一个进程有机会这样做之前,其他一些进程可能会放置MVar。您需要强制执行某种纪律。最常见的方法是始终在放置MVar之前获取它(例如,使用modifyMVar)。 - dfeuer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接