Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are these threads blocked forever?

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 ?

like image 222
Stefan Dorn Avatar asked Jun 09 '20 16:06

Stefan Dorn


1 Answers

Some basic rules about concurrency in Haskell:

  1. 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.

  2. 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, and ThreadKilled, and passes all other exceptions to the uncaught exception handler.

    -- The Control.Concurrent.forkIO documentation

  3. 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:

  1. the main thread exits too quickly for that too actually happen;

  2. when non-main threads are killed by BlockedIndefinitelyOnMVar, they do not print the exception, you have to do so yourself.

like image 62
Li-yao Xia Avatar answered Oct 10 '22 04:10

Li-yao Xia