的签名modifyIORef
很简单:
modifyIORef :: IORef a -> (a -> a) -> IO ()
不幸的是,这不是线程安全的。有一个替代方案可以解决这个问题:
atomicModifyIORef :: IORef a -> (a -> (a,b)) -> IO b
这两个功能之间究竟有什么区别?b
在修改IORef
可能从另一个线程读取的参数时,我应该如何使用该参数?
的签名modifyIORef
很简单:
modifyIORef :: IORef a -> (a -> a) -> IO ()
不幸的是,这不是线程安全的。有一个替代方案可以解决这个问题:
atomicModifyIORef :: IORef a -> (a -> (a,b)) -> IO b
这两个功能之间究竟有什么区别?b
在修改IORef
可能从另一个线程读取的参数时,我应该如何使用该参数?
额外参数用于提供返回值。例如,您可能希望能够自动替换存储在 a 中的值IORef
并返回旧值。你可以这样做:
atomicModifyIORef ref (\old -> (new, old))
如果您没有要返回的值,则可以使用以下内容:
atomicModifyIORef_ :: IORef a -> (a -> a) -> IO ()
atomicModifyIORef_ ref f =
atomicModifyIORef ref (\val -> (f val, ()))
它具有与 相同的签名modifyIORef
。
这就是我的理解。想想括号成语后面的函数,例如
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
这些函数将函数作为参数并返回该函数的返回值。atomicModifyIORef
与此类似。它接受一个函数作为参数,目的是返回该函数的返回值。只有一个复杂之处:参数函数还必须返回一个新值以存储在IORef
. 因此,atomicModifyIORef
需要从该函数返回两个值。当然,这个案例与括号案例并不完全相似(例如没有IO
涉及,我们不处理异常安全等),但是这个类比给了你一个想法。
正如您在评论中所说,如果没有并发,您就可以编写类似的东西
modifyAndReturn ref f = do
old <- readIORef ref
let !(new, r) = f old
writeIORef r new
return r
但是在并发上下文中,其他人可以更改读取和写入之间的引用。
我喜欢查看这个的方式是通过State
monad。有状态的操作会修改一些内部状态并额外产生一个输出。这里状态在一个内部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)