
#9347: forkProcess does not acquire global handle locks -------------------------------------+------------------------------------- Reporter: edsko | Owner: Type: bug | Status: new Priority: normal | Milestone: Component: Compiler | Version: 7.8.2 Keywords: | Differential Revisions: Operating System: Unknown/Multiple | Architecture: Type of failure: None/Unknown | Unknown/Multiple Test Case: | Difficulty: Unknown Blocking: | Blocked By: | Related Tickets: -------------------------------------+------------------------------------- The global I/O handles (`stdout`, `stdin`, `stderr`) all make use an `MVar` wrapping a `Handle__`, and many I/O functions temporarily take this `MVar` (for instance, functions such as `hPutStr` include a call to `wantWritableHandle`, which uses `withHandle_'`, which involves taking the `MVar`, executing some operation, and then putting the `MVar` back). Suppose we have a program consisting of two threads A and B, where thread A is doing I/O. If thread B does a call to `forkProcess` then it is possible that the `fork()` happens at the point that A has just taken, say, the `MVar` for `stdout`. If this happens, every use of `stdout` in the child process will now forever deadlock. This is not a theoretical scenario. The example code reported by Michael Snoyman a few years ago http://www.haskell.org/pipermail/haskell-cafe/2012-October/103922.html exhibits precisely this behaviour: the child process deadlocks (not all the the time, but very frequently), exactly because of this problem. In `forkProcess` we avoid this sort of situation for all of the global RTS locks by acquiring the lock just before the call to `fork()`, and then releasing the lock in the parent again and re-initializing the lock in the child. But there are no provisions for Haskell-land locks such as the above `MVar`. In principle we can work around this problem entirely in user-land. Here is a modified version of Michael's code that does not deadlock (at least, it never has in my tests..), that basically takes the same acquire- release*2 trick that `forkProcess` does for RTS locks in the lines marked `(*)`: {{{ import System.Posix.Process (forkProcess, getProcessID) import Control.Concurrent (forkIO, threadDelay) import System.IO (hFlush, stdout) import System.Posix.Signals (signalProcess, sigKILL) import Control.Exception (finally) import Control.Concurrent import GHC.IO.Handle.Types import System.IO main :: IO () main = do mapM_ spawnChild [1..9] ioLock <- lockIO -- (*) child <- forkProcess $ do unlockIO ioLock -- (*) putStrLn "starting child" hFlush stdout loop "child" 0 unlockIO ioLock -- (*) print ("child pid", child) hFlush stdout -- I've commented out the "finally" so that the zombie process stays alive, -- to prove that it was actually created. loop "parent" 0 -- `finally` signalProcess sigKILL child spawnChild :: Int -> IO () spawnChild i = do _ <- forkIO $ loop ("spawnChild " ++ show i) 0 return () loop :: String -> Int -> IO () loop msg i = do pid <- getProcessID print (pid, msg, i) hFlush stdout threadDelay 1000000 loop msg (i + 1) -------------------------------------------------------------------------------- lockIO :: IO Handle__ lockIO = case stdout of FileHandle _ m -> takeMVar m unlockIO :: Handle__ -> IO () unlockIO hout = case stdout of FileHandle _ m -> putMVar m hout }}} I guess that _any_ global `MVar` or `TVar` is suspect when using `forkProcess`. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/9347 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler