modifyIORef
的签名足够简单:
modifyIORef :: IORef a -> (a -> a) -> IO ()
不幸的是,这不是线程安全的。有一个替代方法可以解决这个问题:
atomicModifyIORef :: IORef a -> (a -> (a,b)) -> IO b
这两个函数到底有什么不同?当修改可能从另一个线程读取的IORef
时,我应该如何使用b
参数?
额外的参数用于提供返回值。例如,您可能希望能够自动替换存储在IORef
中的值并返回旧值。你可以这样做:
atomicModifyIORef ref (old -> (new, old))
如果没有要返回的值,可以使用以下命令:
atomicModifyIORef_ :: IORef a -> (a -> a) -> IO ()
atomicModifyIORef_ ref f =
atomicModifyIORef ref (val -> (f val, ()))
与modifyIORef
具有相同的签名。
正如您在评论中所述,如果没有并发性,您就可以编写像
这样的内容modifyAndReturn ref f = do
old <- readIORef ref
let !(new, r) = f old
writeIORef r new
return r
但是在并发上下文中,其他人可以改变读和写之间的引用
我是这样理解的。考虑遵循括号习惯的函数,例如
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
这些函数接受一个函数作为实参并返回该函数的返回值。atomicModifyIORef
与此相似。它接受一个函数作为参数,目的是返回该函数的返回值。只有一个复杂的地方:参数函数也必须返回一个新值来存储在IORef
中。因此,atomicModifyIORef
要求该函数返回两个值。当然,这种情况与括号情况并不完全相似(例如,没有涉及IO
,我们不处理异常安全等),但是这个类比给了您一个想法。
我喜欢通过State
单子来查看这一点。有状态操作修改一些内部状态,并产生一个输出。这里状态在IORef
中,结果作为IO
操作的一部分返回。因此,我们可以使用State
将函数重新表述如下:
import Control.Monad.State
import Data.IORef
import Data.Tuple (swap)
-- | Applies a stateful operation to a reference and returns its result.
atomicModifyIORefState :: IORef s -> State s a -> IO a
atomicModifyIORefState ref state = atomicModifyIORef ref (swap . runState state)