Appendix: Tales of Two Handles and a Segfault

module Main where

import Control.Concurrent (forkIO, threadDelay)
import Control.Monad (forM_, when)
import Data.IORef (newIORef, readIORef, writeIORef)
import System.IO
import System.Mem (performGC)
import System.Process (createPipe)

main :: IO ()
main = do
  (readH, writeH) <- createPipe
  putStrLn $ "opened pipe  read=" <> show readH <> "  write=" <> show writeH

  -- Hand readH to a thread that does nothing but die after 500ms.
  -- Once the thread exits, readH is unreachable and eligible for GC.
  closeThread <- newIORef False
  let
    go = do
      threadDelay 100_000
      shouldClose <- readIORef closeThread
      if shouldClose
        then putStrLn $ "reader thread exiting (dropping " <> show readH <> ")"
        else go

  _ <- forkIO go

  -- If the `readH` isn't explicitly kept alive for the duration of `forM_`,
  -- it'll cause a failure when reading `writeH`.
  forM_ [1 :: Int .. 10] $ \i -> do
    _ <- hPutStr writeH ("tick " <> show i) >> hFlush writeH
    putStrLn $ "write #" <> show i <> " ok"
    -- After some time, close the read thread so it gets GC'd
    when (i == 2) $ do
      _ <- writeIORef closeThread True
      putStrLn $ "shutting down read thread"
      pure ()
    threadDelay 100_000
    performGC
  putStrLn "write done"