Multithreading and gtk2hs

160 views Asked by At

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?

2

There are 2 answers

0
Daniel Wagner On BEST ANSWER

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.

0
Patrick Collins On

Daniel Wagner answered my question as asked, but I got a more informative perspective from the #haskell IRC channel, which I'll post here for future reference.

Rather than jumping through awkward hoops of forking off the GUI thread and putting the main thread to sleep, a better solution is to let the main thread be the GUI thread and deal with the reactive-banana event network in a new thread. I ended up modifying my main function to contain the following:

keyChan <- newChan
_ <- forkIO $ watchKeys keyPress keyChan
_ <- win `on` keyPressEvent $
    eventKeyVal >>= liftIO . writeChan keyChan >> return True

where watchKeys is defined as:

watchKeys :: EventSource KeyVal -> Chan KeyVal -> IO ()
watchKeys keyPress chan = forever $
    readChan chan >>= fire keyPress 

Now I can deal with the postGUI(A)Sync issues in exactly one place, by defining:

reactimateSafe :: Frameworks t => Event t (IO ()) -> Moment t ()
reactimateSafe = reactimate . fmap postGUIAsync

and using reactimateSafe for any IO action that modifies a GTK object