Writing multi-threaded GUIs
So what’s the problem?
Remember that with these GUI toolkits the program is driven by the event loop. And that event loop is written in C so once our Haskell program enters the event loop it does not get a chance to run any Haskell code except when event handlers are running. So that means all the other Haskell threads would not get a chance to run.
So the challenge is to give the Haskell threads a chance to run.
What we are dong with this little bit of code is using a feature of the Gtk main loop which installs a timer that calls us back a regular intervals. In this case we’re asking to be called back every 50 milliseconds (20 times a second). Once control is back in Haskell land we use the concurrency primitive
yield to yield to any other Haskell threads that are currently runnable. After giving other threads a chance to run for a bit we return to C land. (The
return True is just to indicate to Gtk that we would like the timer to trigger again rather than stopping.)
So what we are really doing is polling the Haskell runtime system and giving it a chance to run any threads it might have. Such polling are generally unsatisfactory. For one thing they do work when there is no work to be done. If there are no runnable Haskell threads then the timeout is just wasted effort. This effort can be reduced by polling less frequently but then conversely that increases latency. For example in our IRC client we would like to respond quickly to a message coming over the network from the server by displaying is on the screen. However if we set our polling frequency too low, say only twice a second, then our IRC program will appear sluggish since it will take up to half a second longer to display incoming messages than other IRC clients. This latency issue can be even more of a problem in other applications.
Now in practice the yield primitive seems to be pretty good at not doing much work when there is no work to be done and so even with it happening 20 times a second the program does not use much CPU time when it is idle. And polling 20 times a second should give fairly low latency, it’s at least comparable to the latency of the Haskell runtime system itself.
Why not just use the threaded Haskell runtime system?
That seems an appealing idea since you could run the Gtk main loop in one thread and still have the Haskell runtime system carry on in another thread. However…
Gtk and other similar toolkits are not thread safe, that is you cannot call GUI functions from multiple OS threads and expect it to just work. (It is possible to use multiple OS threads by taking advantage of some Gtk locking functions but this cannot be done by Gtk2Hs since it would mean locking at the wrong level of granularity and would result in terrible performance.) since we don’t want to make using threads complex by requiring users understand locking then this is not a reasonable approach.
It would all be fine if we could guarantee that all Haskell threads that want to use the GUI were bound to a single OS thread. Unfortunately the bound threads system does not help. The current bound threads system allows you to fork a new Haskell thread bound to a new OS thread. This is just what you need for OpenGL. However for UI toolkits like Gtk, we need to fork a new Haskell thread bound to an existing OS thread. Although even if we could do that we would be back in the same situation as the single threaded case of having to use our polling technique (since the one GUI OS thread would still be busy with the event loop most of the time).
Finally, it should be possible to eliminate the polling and do something more satisfactory. To get the program to genuinely do nothing when it is idle requires that we know all the events that would cause us it to wake up. For example these events might be timers expiring, or data arriving on a socket. The Gtk event loop knows all the event sources that it is interested in (mostly messages from the X server). The Haskell runtime system knows all the event sources that it is interested in (which it manages on behalf of all the Haskell threads). So we have two event systems each with a complete view of their little world. However neither has a complete view of the program as a whole. Because of that neither event system can block for long periods of time without causing the other system to miss events.
If there were some way of getting the Haskell runtime to tell us about the event sources and timeouts that it is watching, then it would be possible to inform the Gtk event loop about these. This would give the Gtk event loop a complete picture of all the event sources for the whole program and so it would be safely be able to block waiting for an event from any of the event sources. It would all “Just Work”™.
Pages: 1 2