multithreadinghaskellconcurrencydeadlocksocat

Why doesn't this program return? Or is it a deadlock?


Here's the full program¹

module Main where

import Control.Concurrent.Async
import Control.Concurrent.MVar
import System.Environment
import System.IO
import System.Process

main :: IO ()
main = do
  args <- getArgs
  (Just i, Just o, Nothing, p) <- createProcess (proc "socat" args)
                                                {std_in = CreatePipe, std_out = CreatePipe}
  sequence_ [hSetBuffering h NoBuffering | h <- [i, o, stdin, stdout]]
  hSetEcho stdin False
  mine <- newEmptyMVar
  res <- concurrently
    (do c <- getChar
        putMVar mine c
        hPutChar i c)
    (do other <- hGetChar o
        mine' <- takeMVar mine
        return (mine', other))
  print $ snd res
  terminateProcess p

I launch two instances of it in different terminals, like this:

$ cabal run myprogram -- TCP-LISTEN:12345,fork -       # in terminal 1
$ cabal run myprogram -- TCP-CONNECT:localhost:12345 - # in terminal 2

(in that order) then I hit one key in each terminal (doesn't matter the order), and they will both print those two keys in a pair (with sides swapped).

Sometimes, though if I hit the key in terminal 1 first, then the process in terminal 1 doesn't return (while the one in terminal 2 does).

Being not particularly experienced with concurrent programming, I wouldn't be surprised for a deadlock, but I don't see how this can be one! Here's my observations/reasoning:


(¹) In this very stripped down example, the frequency with which this happens is relatively low, but not too much (a few tens of attempts seems to suffice). I do have a slightly more nosisy example that seems to be impacted a bit more, but I don't think there's a "structural" difference with respect to this one, so I haven't posted it to keep it simpler, in case the reason for the observed behavior is apparent to the experts, but I can post it if deemed useful.


Solution

  • Here is one sequence of events that lead to the observed behavior:

    now both threads in app2 are done so:

    finally: