I am currently reading Simon Marlow's book "Parallel and Concurrent Programming in Haskell" and I don't understand this code:
waitAny :: [Async a] -> IO a
waitAny as = do
m <- newEmptyMVar
let forkwait a = forkIO $ do r <- try (wait a); putMVar m r
mapM_ forkwait as
wait (Async m)
Here we call putMVar N times but we have only 1 wait operation. Do I understand it correct that N-1 threads will be blocked trying to execute putMVar? What is happening here?
...or super-simplified:
test = do
m <- newEmptyMVar
forkIO $ putMVar m 1
forkIO $ putMVar m 2
a <- readMVar m
return a
Why does it work without problems? Why don't I have Exception: thread blocked indefinitely in an MVar operation ?
Some basic rules about concurrency in Haskell:
When the main
thread exits, it immediately kills all of the other threads with it. You have to explicitly wait for the other threads if you want to give them the opportunity to clean up.
There is a particular set of exceptions that auxiliary (non-main) threads discard, so they are not printed when they are uncaught:
The newly created thread has an exception handler that discards the exceptions
BlockedIndefinitelyOnMVar
,BlockedIndefinitelyOnSTM
, andThreadKilled
, and passes all other exceptions to the uncaught exception handler.-- The
Control.Concurrent.forkIO
documentation
When a thread waits on an MVar
that has no hope of making any progress, it receives an exception. But because of the above issues, this is entirely invisible in this example. Note that only a very simple class of deadlocks are caught this way, thanks to special support in the garbage collector. It's not possible to detect all deadlocks automatically.
In your second example, the main thread (assuming main = test
) exits immediately after reading the variable, which leaves no time for the other thread (the one still blocked on putMVar
) to react (point 1 above). So first add a threadDelay
at the end of the main thread to give some more time to the other thread. That is not yet enough to see a difference, because auxiliary threads are killed by BlockedIndefinitelyOnMVar
silently (point 2). Add an exception handler around putMVar
to produce an explicit output.
import Control.Concurrent
import Control.Exception
main :: IO ()
main = do
m <- newEmptyMVar :: IO (MVar Int)
forkIO $ putMVar' m 1
forkIO $ putMVar' m 2
a <- readMVar m
print a
threadDelay 1000000 -- (1) Wait for other threads to clean up
putMVar' :: MVar Int -> Int -> IO ()
putMVar' r x =
catch
(putMVar r x)
(\e ->
putStrLn ("BLOCKED: " ++ show (x, e :: SomeException))) -- (2) Print something if the thread dies because of a deadlock
{- Build this file with ghc -threaded ThisFile.hs
Run it with ./ThisFile +RTS -N
-}
{- Output:
1
BLOCKED: (2,thread blocked indefinitely in an MVar operation)
-}
Note that forkIO
should generally be avoided because it is so low level. It requires a lot of effort to implement synchronization from scratch. The async library provides more convenient abstractions.
To recapitulate and technically answer your questions:
Here we call putMVar N times but we have only 1 wait operation. Do I understand it correct that N-1 threads will be blocked trying to execute putMVar? What is happening here?
That is the right idea. In practice, the blocked threads will get an exception because the garbage collector can see that the MVar
is reachable from no other thread, but you are not supposed to catch and observe that exception in production, even though it's possible as shown above. Indeed, the documentation of Control.Concurrent
says that much:
Note that this feature is intended for debugging, and should not be relied on for the correct operation of your program.
-- The
Control.Concurrent
documentation
Why does it work without problems? Why don't I have
Exception: thread blocked indefinitely in an MVar operation
?
There will actually be such an exception, but:
the main
thread exits too quickly for that too actually happen;
when non-main
threads are killed by BlockedIndefinitelyOnMVar
, they do not print the exception, you have to do so yourself.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With