Haskell, GTK and Multi-Threading
I have been working on an application in Haskell, using Gtk2Hs for the user interface. Now, you normally want a graphical user interface (GUI) to be responsive, so you avoid doing tasks that take a long time in the thread that handles the GUI. Instead, your main computation happens in other application threads, and all that happens in the GUI thread is updating of interface elements. It turns out that there are several issues that come up when you mix Haskell, GTK and multiple threads, which is why this post is here.
Part 1: The GTK event loop
Most programs that use Gtk2Hs first do all the GUI-related initialization, and then execute the mainGUI computation. This is actually a loop that processes GTK-related events until the user quits the program. Because of the way thread switching works in Haskell, a thread executing a loop like that will not let any other “lightweight” threads run. (Lightweight threads are threads created using the forkIO computation.) One frequently suggested way to solve this problem is to make the GTK event loop periodically yield to any other Haskell threads by adding the following to your GUI initialization code:
timeoutAddFull (yield >> return True) priorityDefaultIdle 100
Note that this is only an issue when all lightweight Haskell threads run on top of a single operating system thread, as in the single-threaded RTS of GHC. If Haskell threads are allowed to run on multiple OS threads, then yielding is not necessary.
Part 2: GTK thread safety
It turns out that that GTK (the C library wrapped by Gtk2Hs) is not thread-safe. What this means is that all modifications of GTK state must happen from a single OS thread, which also must be the same thread that is executing the GTK event loop. If all Haskell threads are run on top of a single OS thread, this is easy to ensure. However, if you use a system where lightweight threads may be mapped to different OS threads (such as in the multi-threaded RTS in GHC), care must be taken when accessing a GUI. Essentially, application threads need to put GUI modification events onto a queue, with the thread that runs the GTK event loop processing the events from that queue. Fortunately, Gtk2Hs comes with such a queue built-in; you can use the postGUI functions to give the event loop blocks of IO to execute.
Note that, by default, Gtk2Hs will produce a warning when running under the GHC multi-threaded RTS. To get rid of this, useĀ unsafeInitGUIForThreadedRTS instead of the usual initGUI to perform GTK initialization. The “unsafe” part of the computation name signifies that you are aware of the requirement to only modify GTK state from the correct OS thread.
Part 3: Deconstructing the GTK event loop
There might come a time when you want to do something more complicated than the above solutions allow. In such a case, you can actually substitute the mainGUI event loop with your own. Gtk2Hs provides functions that will process a single event at a time off the GTK event queue. Put these in a loop, sprinkle with your custom logic (such as pulling events off multiple queues), and you’re done!
Context
This was written with the GHC environment in mind. Other Haskell compilers and/or interpreters may differ in their implementations.
Steve says:
April 4th, 2009 at 4:42 am
> Instead, your main computation happens in other application threads, and all that happens in the GUI thread is updating of interface elements.
I find it unfortunate that a common perception is that threads are a good approach to this problem. While this does work, this approach to threading makes you dance around locking issues just like this article describes.
A better way to handle GUI updates is to use an asynchronous approach. That is, tasks that do IO should use asynchronous IO so that the GUI can remain responsive while something is happening. Tasks that require long computations should launch threads _as workers_ that will come back with an answer. The GUI thread should not wait on these threads by launch them as asynchronous services, and provide a function or message handler that will be called when the task is complete rather than waiting around for an answer. This works just as well by talking to another _process_ instead of a thread, and then you can use IPC methods for communication to ensure safety. Using sockets for communication between processes can even be used in an asynchronous (non-blocking) manner, which might provide _better_ performance than what you would get from copious use of threads and the accompanying sync locking.
Another approach is not to launch threads at all but instead break up the computational loop into several callbacks of a timer, or idle function. This is a way to do asynchronous processing (not blocking the GUI) without even needing to launch threads. Using threads or processes only helps in two ways:
- you can use a local continuous loop for computation, making state management easier
- the operating system knows how to split processes and threads onto multiple CPU cores
Avoiding the misuse of threads and instead learning to think asynchronously will save you BIG headaches in the future, I promise. This is made even easier (than C) in functional languages and languages that support closures because writing asynchronous callbacks using closures is a snap.
Sebastian says:
April 4th, 2009 at 10:41 am
Steve, I find most asynchronous programming to be incredibly painful. Haskell’s approach is to have a very light-weight threading system which means you can write synchronous code in multiple threads instead which is a far better experience in my opinion. You don’t *need* to worry explicitly about asynchronous programming precisely because you have (efficient) threads. I don’t see how moving to an even lower level of abstraction is helping.
F# doesn’t have lightweight threads so they offer kind of a specialized solution using an “async” monad which allows you to write synchronous-looking code that’s still asynchronous when run. The core principle is the same though – the programmer shouldn’t have to worry about the low-level details of asynchronous programming.