I was doing some experiments with concurrency and memory visibility and ran into this strange behavior (see comments inline):
module Main
where
import Data.IORef
import Control.Concurrent
import System.CPUTime
import System.IO
main = do
hSetBuffering stdout NoBuffering
r <- newIORef False
putStrLn "forking..." -- PRINTED
forkIO $ f r
threadDelay 1000000
putStrLn "writeIORef" -- NEVER PRINTED
writeIORef r True
threadDelay maxBound
f :: IORef Bool -> IO ()
f r = readIORef r >>= \b-> if b then print "NEVER PRINTED" else f r
I was expecting perhaps the writeIORef
not to be visible to the child thread, but not for the main thread to simply (apparently) stall.
Compiled on ghc 7.8.3
cabal exec ghc -- --make -fforce-recomp -O2 -threaded visibility.hs
and run with
./visibility +RTS -N
What's happening here?
EDIT: So my machine has two real cores and two hyperthreading cores, so with +RTS -N
GHC sees 4 capabilities. Per Gabriel Gonzalez's answer I tried out the following to see if maybe the scheduler was putting both threads on the same physical processor:
module Main
where
import Data.IORef
import Control.Concurrent
import GHC.Conc(threadCapability,myThreadId,forkOn)
main = do
r <- newIORef False
putStrLn "going..."
(cap,_) <- threadCapability =<< myThreadId
forkOn (cap+1) $ f r -- TRIED cap+1, +2, +3....
threadDelay 1000000
putStrLn "writeIORef" -- BUT THIS STILL NEVER RUNS
writeIORef r True
threadDelay maxBound
f :: IORef Bool -> IO ()
f r = readIORef r >>= \b-> if b then print "A" else f r
It looks like this is probably an ancient ghc bug #367.
ghc
only suspends threads at well-defined safe points, which are only when memory is allocated. I believe your forked thread never allocates memory, so it never relinquishes control to other threads. Therefore, your main thread never progresses once the compiler schedules the forked thread (sometime in the middle of yourthreadDelay
).You can learn more about safe points here in the section on "Lightweight Threads and Parallelism".
Edit: As Thomas mentioned, you can use
Control.Concurrent.yield
to explicitly relinquish control when you encounter situations like these.