这对我来说完全是一个惊喜。readIORef
当有atomicModifyIORef
飞行时,有人可以解释阻塞背后的原因是什么吗?我知道假设是提供给后一个函数的修改函数应该非常快,但这不是重点。
这是一个示例代码,它重现了我正在谈论的内容:
{-# LANGUAGE NumericUnderscores #-}
module Main where
import Control.Concurrent
import Control.Concurrent.Async
import Control.Monad
import Data.IORef
import Say (sayString)
import Data.Time.Clock
import System.IO.Unsafe
main :: IO ()
main = do
ref <- newIORef (10 :: Int)
before <- getCurrentTime
race_ (threadBusy ref 10_000_000) (threadBlock ref)
after <- getCurrentTime
sayString $ "Elapsed: " ++ show (diffUTCTime after before)
threadBlock :: IORef Int -> IO ()
threadBlock ref = do
sayString "Below threads are totally blocked on a busy IORef"
race_ (forever $ sayString "readIORef: Wating ..." >> threadDelay 500_000) $ do
-- need to give a bit of time to ensure ref is set to busy by another thread
threadDelay 100_000
x <- readIORef ref
sayString $ "Unblocked with value: " ++ show x
threadBusy :: IORef Int -> Int -> IO ()
threadBusy ref n = do
sayString $ "Setting IORef to busy for " ++ show n ++ " μs"
y <- atomicModifyIORef' ref (\x -> unsafePerformIO (threadDelay n) `seq` (x * 10000, x))
-- threadDelay is not required above, a simple busy loop that takes a while works just as well
sayString $ "Finished blocking the IORef, returned with value: " ++ show y
运行这段代码会产生:
$ stack exec --package time --package async --package say --force-dirty --resolver nightly -- ghc -O2 -threaded atomic-ref.hs && ./atomic-ref
Setting IORef to busy for 10000000 μs
Below threads are totally blocked on a busy IORef
readIORef: Wating ...
Unblocked with value: 100000
readIORef: Wating ...
Finished blocking the IORef, returned with value: 10
Elapsed: 10.003357215s
请注意,readIORef: Wating ...
它只打印两次,一次是在阻塞之前,一次是在阻塞之后。这是非常出乎意料的,因为它是一个在完全独立的线程中运行的动作。这意味着阻塞IORef
会影响其他线程而不是调用的线程readIORef
,这更令人惊讶。
这些语义是预期的,还是一个错误?我适合不是错误,为什么这是预期的?稍后我会打开一个 ghc 错误,除非有人对此行为有我想不出的解释。这是 ghc 运行时的一些限制,我不会感到惊讶,在这种情况下,我稍后会在这里提供答案。无论结果如何,了解这种行为都非常有用。
编辑 1
在评论中请求了我尝试过的不需要的繁忙循环unsafePerformIO
,所以在这里
threadBusy :: IORef Int -> Int -> IO ()
threadBusy ref n = do
sayString $ "Setting IORef to busy for " ++ show n ++ " μs"
y <- atomicModifyIORef ref (\x -> busyLoop 10000000000 `seq` (x * 10000, x))
sayString $ "Finished blocking the IORef, returned with value: " ++ show y
busyLoop :: Int -> Int
busyLoop n = go 1 0
where
go acc i
| i < n = go (i `xor` acc) (i + 1)
| otherwise = acc
结果完全相同,只是运行时略有不同。
Setting IORef to busy for 10000000 μs
Below threads are totally blocked on a busy IORef
readIORef: Wating ...
Unblocked with value: 100000
readIORef: Wating ...
Finished blocking the IORef, returned with value: 10
Elapsed: 8.545412986s
编辑 2
事实证明,这sayString
是没有输出没有出现的原因。这是 outsayString
被交换时的内容putStrLn
:
Below threads are totally blocked on a busy IORef
Setting IORef to busy for 10000000 μs
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
readIORef: Wating ...
Finished blocking the IORef, returned with value: 10
Unblocked with value: 100000
Elapsed: 10.002272691s
那仍然没有回答问题,为什么要readIORef
阻止。事实上,我偶然发现了 Samuli Thomasson 的《Haskell High Performance》一书中的一句话,它告诉我们不应该发生阻塞: