Written: 2020-09-22
Threads and blocking calls
I've written a little script to cement my understanding of GHC's rts w.r.t. threading. The script can be run in a few different configuration and will report which OS threads (aka capabilities) are blocked.
For example, calling a blocking C function marked as "safe" should not block other threads:
$ ./blocking safe +RTS -N4
Cap 3 is NOT blocked.
Cap 2 is NOT blocked.
Cap 1 is NOT blocked.
Cap 0 is NOT blocked.
If we accidentally mark a blocking C function as "unsafe", though, this will block the calling thread but not other threads:
$ ./blocking unsafe +RTS -N4
Cap 3 is NOT blocked.
Cap 2 is NOT blocked.
Cap 1 is NOT blocked.
Cap 0 is blocked.
(Note that main is not always on capability 0.)
GHC's uses a stop-the-world GC for nurseries and the minor generation. Accordingly, triggering a GC while any thread is blocked should block all threads:
$ ./blocking unsafe gc +RTS -N4
Cap 3 is blocked.
Cap 2 is blocked.
Cap 1 is blocked.
Cap 0 is blocked.
Local heaps
Having local heaps (ie nurseries that are collected independently of each other) would eliminate the cost of synchronizing every garbage collect. Local heaps were implemented a long time ago by Simon Marlow and Simon Peyton Jones but the results weren't good enough for mainline GHC. It might be time to test this approach again since maintaining the right invariants is a lot easier with a fixed-size nursery than for an entire minor generation.
Appendix: blocking.hs
{-# LANGUAGE ForeignFunctionInterface #-}
module Main (main) where
import Control.Concurrent
( forkOn,
getNumCapabilities,
myThreadId,
threadDelay,
)
import Control.Monad (forM_, when)
import Foreign.C (CInt (..))
import System.Console.ANSI
( clearFromCursorToLineEnd,
cursorDownLine,
cursorUpLine,
)
import System.Environment (getArgs)
import System.IO (hFlush, stdout, hClose)
import System.IO.Unsafe (unsafePerformIO)
import System.Mem (performMajorGC)
foreign import ccall safe "sleep" safe_sleep :: CInt -> IO CInt
foreign import ccall unsafe "sleep" unsafe_sleep :: CInt -> IO CInt
main :: IO ()
main = do
n <- getNumCapabilities
forM_ (reverse [0 .. n -1]) $ \hec ->
putStrLn $ "Cap " ++ show hec ++ " is blocked."
forM_ [0 .. n -1] $ \hec -> do
forkOn hec (counter hec)
if hasArg "unsafe"
then unsafe_sleep 2
else safe_sleep 2
hClose stdout
return ()
counter :: Int -> IO ()
counter n = do
threadDelay (10 ^ 5)
when (hasArg "gc") performMajorGC
tid <- myThreadId
threadDelay (10 ^ 5 * n)
cursorUpLine (n + 1)
clearFromCursorToLineEnd
putStr $ "Cap " ++ show n ++ " is NOT blocked."
cursorDownLine (n + 1)
hFlush stdout
hasArg :: String -> Bool
hasArg key = unsafePerformIO $ elem key <$> getArgs