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:
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):
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:
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:
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.
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:
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