Writing multi-threaded GUIs

Using many threads can be a good way to structure GUI programs. Some things that are tricky to do using traditional event loop techniques become easy when using threads. Haskell has excellent support for using multiple threads so it ought to be a natural fit; to do multi-threaded GUI programming in Haskell. However there are some issues and limitations with the current threading support.

This article demonstrates a practical approach to creating multi-threaded GUIs with Gtk2Hs and tries to explain the issues and limitations.

Why use threads?

Most GUI systems, including Gtk+, are based on an “event loop”. This deals with incoming events from the user (like mouse movements and keyboard input) and dispatches these events to the appropriate code that implements the program’s reaction to the event. For example we can arrange for something to happen when the user clicks a button with the mouse:

button `onClicked` do
  putStrLn "Hello World!"

No other events can be processed until the action associated with the event has finished. For this reason it is important that the action not take too long to run. Users will complain that our GUI applications feel “sluggish” if they do not respond to user input within a fairly small fraction of a second.

This is obviously OK in the above example but what if pressing the button is supposed to start a long running computation, like ray tracing for example?

There are other situations where the event loop can get in the way. While a non-GUI program might be able to wait for incoming messages on a network socket using traditional blocking read operations, that is not possible when you’ve got to keep the event loop going to stop the GUI from freezing up. There is a solution to this in the event loop systems which is to turn the event of data arriving on a pipe/socket into just another event in the event loop. However this does not always make for an elegant style of programming if there is a lot of state to remember from the processing of one event to the next (since it all has to be explicitly saved somewhere).

In both of these situations, using threads can allow a more elegant programming style.

A multi-threaded GUI application

We’ll look at the example of HRay, a Haskell raytracing program. The first version of HRay suffers from the problem that when you press the “Render scene” button the GUI freezes until the rendering has finished (which can be several minutes).

The original code looks like this (albeit rather simplified):

onClicked renderButton $ do
  pixBuf <- renderScene renderDescr descrLabel
  imageSetFromPixbuf image pixBuf

It’s the renderScene bit that takes a long time of course. So what we’d like to do is run that bit in a separate thread so the main thread can get back to dealing with the GUI event loop. So we use forkIO to start another thread:

onClicked renderButton $ do
  forkIO $ do
    pixBuf <- renderScene renderDescr descrLabel
    imageSetFromPixbuf image pixBuf
  return ()

Now, there is one more vital thing to do to get this to work. We have to add the following bit of code into the program. It’s easiest to stick it in the GUI initialization code:

main :: IO ()
main = do
  initGUI
  timeoutAddFull (yield >> return True)
                 priorityDefaultIdle 50
  ...

For the moment you’ll just have to believe me! We’ll get onto what that line does and why we need it a little later. Now it’s time to bask in the glory of what we’ve achieved. :-)

We probably want to add a bit more like disabling the renderButton while the rendering is going on so that the user can’t start more than one rendering operation at once (since we’ve not really designed it to cope with that). Then it’d also be nice to display an activity bar while the rendering is going on just to reassure the user that something is happening and it’s worth them waiting. If we are following the advice of the GNOME HIG (Human Interface Guidelines) then we should allow the user to cancel the operation. So we should pop up a progress window with a cancel button. If the user presses cancel we can send an asynchronous exception to the thread doing the rendering to make it stop.

Lets move onto another example. This time it’s an IRC client. An IRC client needs to listen for incoming messages from the IRC server. We can do this elegantly by having a thread that listens to incoming messages and updates a TextView widget with the message that was received.

main :: IO ()
main = do
  initGUI
  
  ... -- set up the GUI
  
  -- use our magic threads thingy
  timeoutAddFull (yield >> return True)
                 priorityDefaultIdle 50
  
  -- connect to the server and start listening
  conn <- connectTo server (PortNumber port)
  hSetBuffering conn NoBuffering
  forkIO (watchConn conn buffer)
  ...

So notice we fork off a thread to watch the connections. It’s passed the connection (a Handle) and the buffer of the text view widget. The work we do in that thread is pretty straightforward:

watchConn :: Handle -> TextBuffer -> IO ()
watchConn conn textbuf = do
  raw <- hGetLine conn
  let message = IRC.decodeMessage raw
  processed <- handleMsg conn message
  displayText textbuf processed
  watchConn conn textbuf

Note how we can just use ordinary blocking IO actions like hGetLine rather than having to use an event loop’s handle watching features. Although in this example the handling of incoming messages is stateless it would be easy to accumulate information from one message to use in processing the next just by adding another parameter to the watchConn function. Compare that with an event loop system where one would have to stash all state into some data structure (probably requiring the use of an IORef) and restore it upon the next event.

Read on for an explanation of what that timeoutAdd thing is doing and all the gory implementation details which explain why we need it…

Pages: 1 2

2 Responses to “Writing multi-threaded GUIs”

  1. Antti-Juhani Kaijanaho Says:

    Gtk+2 makes it possible to “deconstruct” its main loop and to drive it from outside of Gtk+. Any reason that’s not used instead of this clever but a bit hackish solution?

  2. Duncan Says:

    That is true, we could drive the Gtk+ main loop from within Haskell. I was about to say that this would still mean having to use polling, however by breaking the Gtk+ main loop into phases it would allow us to get at the set of file descriptors that Gtk+ is waiting on. In which case we might be able to do the blocking in Haskell rather than in the Gtk+ loop which would mean the Haskell runtime system would have a complete view of the programs event sources.

    Thank you for the idea. I’ll certainly investigate it. :-)