multithreadinghaskellgtkreactive-banana

Multithreading and gtk2hs


I'm writing some code with reactive-banana and gtk2hs that needs to read from a file handle. I need to have at least two threads (one to read keyboard events with reactive banana and one to read from the file handle), so at the moment I have code that looks something like this:

type EventSource a = (AddHandler a, a -> IO ())

fire :: EventSource a -> a -> IO ()
fire = snd

watch :: EventSource ByteString -> Handle -> IO ()
watch textIn pty = forever $
  hGetLine pty >>= fire textIn >> threadWaitRead pty

With the following main function:

mainAxn :: IO ()
mainAxn = do
  h <- openFile "foo" ReadMode

  initGUI

  win <- windowNew
  txt <- textViewNew

  containerAdd win txt

  widgetShowAll win

  (keyPress, textIn) <-
    (,) <$> newAddHandler <*> newAddHandler
  network <- setupNetwork keyPress textIn
  actuate network

  _ <- forkIO $ watch textIn h

  _ <- win `on` keyPressEvent $
       eventKeyVal >>= liftIO . fire keyPress >> return True

  mainGUI

and my event network set up as follows:

setupNetwork :: EventSource KeyVal -> EventSource ByteString -> IO EventNetwork
setupNetwork keyPress textIn = compile $ do
  ePressed <- fromAddHandler $ addHandler keyPress
  eText <- fromAddHandler $ addHandler textIn

  reactimate $ print <$> (filterJust $ keyToChar <$> ePressed)
  reactimate $ print <$> eText

(except in my actual code, those reactimate calls write to the TextView built in mainAxn). I found that I needed to build with -threaded to make the event network correctly capture both text from textIn and keypresses from keyPress, which caused issues because it's not safe to modify objects from the gtk package concurrently.

At the moment, I have postGUIAsync calls scattered throughout my code, and I've found that using postGUISync causes the whole thing to deadlock --- I'm not sure why. I think it's because I end up calling postGUISync inside of the same thread that ran mainGUI.

It seems like it would be better to run all of the GUI stuff in its own thread and use the postGUI* functions for every access to it. However, when I change the last line of mainAxn to be

forkIO mainGUI
return ()

the program returns immediately when it hits the end of mainAxn. I tried to fix that by using:

forkIO mainGUI 
forever $ return ()

but then the gtk GUI never opens at all, and I don't understand why.

What's the right way to do this? What am I missing?


Solution

  • The basic problem here is that, in Haskell, as soon as main exits, the entire program is torn down. The solution is simply to keep the main thread open; e.g.

    done <- newEmptyMVar
    forkOS (mainGUI >> putMVar done ())
    takeMVar done
    

    I've also replaced forkIO with forkOS. GTK uses (OS-)thread-local state on Windows, so as a matter of defensive programming it is best to ensure that mainGUI runs on a bound thread just in case one day you want to support Windows.