关于Haskell并发中MVar内部状态不可变性的疑问解析
关于Haskell中MVar与不可变Map的并发疑问
首先给出书中第133-134页的代码:
type Name = String type PhoneNumber = String type PhoneBook = Map Name PhoneNumber newtype PhoneBookState = PhoneBookState (MVar PhoneBook) lookup :: PhoneBookState -> Name -> IO (Maybe PhoneNumber) lookup (PhoneBookState m) name = do book <- takeMVar m putMVar m book return (Map.lookup name book)
疑问点
在Simon Marlow所著的《Parallel and Concurrent Programming in Haskell》(《Haskell并行与并发编程》)中,作者提到:不可变数据结构结合可变包装器有额外优势,
lookup仅短时间持有锁,后续Map.lookup在锁外执行,利于并发。这部分理解清晰,但后半段“此操作可行因状态值不可变,若数据结构可变则需全程持有锁”令人困惑。疑问是:作者是否假设Map暴露可变API?还是指其他线程修改MVar内容会影响已取出的book?
解答
要搞懂这个点,得从Haskell的不可变性本质和可变数据结构的并发风险两方面来看:
首先,作者说的“可变数据结构”不是指Haskell标准库的
Map——因为Haskell的Map本身就是纯不可变的,不存在可变API。这里的“可变数据结构”是泛指带可变操作的结构(比如其他语言里的HashMap,或者Haskell中用IORef/STRef封装的可变字典)。为什么可变结构需要全程持有锁?
- 如果数据结构是可变的,当你从MVar里取出它之后立刻释放锁(像代码里那样
putMVar m book),其他线程就可能拿到这个可变结构并修改它。而你此时还在执行查找操作,这会导致数据竞争——你的查找可能读到一半被其他线程修改,结果完全不可预测。 - 但Haskell的
Map是不可变的,你从MVar里取出的book是一个固定的快照。哪怕其他线程之后修改了MVar里的PhoneBook(比如插入新条目,本质是生成一个新的Map替换旧的),你手里的book也不会变,所以Map.lookup可以安全地在锁外执行,完全不用担心被干扰。
- 如果数据结构是可变的,当你从MVar里取出它之后立刻释放锁(像代码里那样
总结:作者的对比是不可变数据结构 vs 可变数据结构的并发处理差异。Haskell的
Map不可变,所以取出后就和MVar里的后续修改无关,锁可以早放;但如果是可变结构,取出后还和MVar里的是同一个引用,必须全程持有锁防止其他线程修改,否则会出并发问题。
内容的提问来源于stack exchange,提问作者Enlico
相关产品推荐
相关产品推荐

