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