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:
putStrLn "hello"
right before print $ snd res
;print $ snd res
and terminateProcess p
, the "wrong" behavior is much more frequent;snd res
), surely those 2 threads haven't deadlocked, I think,socat
process, so I don't understand why the behavior should be asymmetrical, in the sense that if there was a deadlock, I'd expect it to happen regardless of what program received the keystroke first;putMVar
and the other is calling takeMVar
on the same single MVar
in that program run? Each of the two calls will block until the other catches up, no?(¹) 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.
Here is one sequence of events that lead to the observed behavior:
now both threads in app2 are done so:
finally: