I’ve been trying to prevent a Python GTK+ application (the system-config-printer printing troubleshooter) from appearing to freeze when performing CUPS operations such as fetching a list of available devices.
Let me describe the problem. In the libcups API the main worker function is cupsDoRequest() and this function blocks until the request is complete. It may need to collect a password from the caller, in which case it will do this with a password callback function set using cupsSetPasswordCB().
The libcups API is used through a set of Python bindings called pycups, and GTK+ is used for the user interface. The object of this game is to allow the GTK+ main loop to continue to run even while the CUPS operation is in progress. I think I’ve got a workable system now.
The solution is to use threads. There are two locks to be aware of when using threads in this environment: the Python global interpreter lock, and the GDK lock.
Step one: allow a separate worker thread for CUPS operations
Python has a single global interpreter lock that ordinarily prevents two threads from running at the same time. In my Python extension module I had to wrap every call to cupsDoRequest() with some code to allow other threads to run. In normal circumstances it would have been sufficient to use the convenience macros, like this:
... Py_BEGIN_ALLOW_THREADS; answer = cupsDoRequest (self->http, request, "/"); Py_END_ALLOW_THREADS; ...
However, in this case the cupsDoRequest() function can call into our callback function if it needs to collect a password. The macros save (and restore) the thread state into a local variable as well as releasing (and taking) the global interpreter lock, so in this case that thread state needs to be stored somewhere else instead, somewhere that my callback function can access it. I chose to store it in the data structure associated with the CUPS connection.
Ideally, the password callback function would be given a pointer to the CUPS connection, with which it could look up the data structure associated with it and retrieve the thread state. Unfortunately this is not the case, at least not yet. As a result, we need to set a global variable to point to the “current” connection. This is safe as long as only one CUPS operation is performed at a time. For the moment that is sufficient, as the task at hand was to have a CUPS operation running in parallel with the GTK+ main loop, not with other CUPS operations.
Instead of Py_BEGIN_ALLOW_THREADS, then, the result of PyEval_SaveThread() is stored in the connection data structure, and a global pointer is set to point to that structure. Instead of Py_END_ALLOW_THREADS, PyEval_RestoreThread() is called, giving it the previously saved thread state.
In the password callback function this is done in reverse: first the saved thread state is restored, then the Python callable object is invoked, and finally the thread state is saved again.
So the full sequence of events looks like this:
- caller invokes pycups method
- pycups saves the thread state and releases the GIL
- pycups calls cupsDoRequest()
- cupsDoRequest calls the pycups password callback
- pycups restores the thread state and re-acquires the GIL
- pycups invokes the caller’s registered password callback (a Python function)
- pycups saves the thread state and releases the GIL, again
- cupsDoRequest may call the password callback several times before returning
- pycups restores the thread state and re-acquires the GIL
That’s the Python global interpreter lock taken care of. Next is the GDK lock.
Step two: deal with the GDK lock
Having dealt with the Python locking, the next stage was to deal with the GDK locking. Any calls to the GTK+ or GDK interfaces must hold the GDK lock. To prepare the printing troubleshooter for using threads, I added a call to gtk.gdk.threads_init(), and then changed the call to gtk.main() so that it acquires the lock:
gtk.gdk.threads_init () gtk.gdk.threads_enter () gtk.main () gtk.gdk.threads_leave ()
Any signal callbacks from GTK+ widgets will be called with the lock already held, but functions called via the GSource mechanism (for example, with g_timeout_add) must perform their own locking.
The next stage was to audit the program to see which functions are called by g_timeout_add(), and add locking to them (i.e. calls to gtk.gdk.threads_enter() and gtk.gdk.threads_leave() whenever GTK+ functions are used).
Finally, the only tricky part left was the authentication dialog. This is a GTK+ dialog that is shown when a password is required for a CUPS operation. The dialog is shown from within a class (authconn.Connection) that is used both by the printing troubleshooter and by the main system-config-printer application, which is not yet ready to use threads. The troubleshooter would use this class from its worker thread when it needed to talk to CUPS, and would need the class to perform GDK locking — but the system-config-printer application would call it from its main thread when the lock was already held. To overcome this I added a method to the authconn.Connection class to instruct it whether to perform locking or not. If locking is required, calls to gtk.gdk.threads_enter/leave() are made when the authentication dialog is shown. The troubleshooter instructs it to do such locking.
Step three: a convenience class
Finally I wanted to make it really easy to start a worker thread, run some Python code, and collect the result. The troubleshooter would do this in many places. The idea was to have a general purpose “long-running operation that can be cancelled if necessary”.
I used the Python “threading” module to do this. By deriving a class from the threading.Thread class, I defined a thread that runs some Python function and stores either the result or the exception if an exception is raised.
Then I created a TimedOperation class which starts the thread running when the class is instantiated. Its “run” method waits for the thread to finish and then returns the result, or raises the exception if an exception was raised.
It currently does this by setting a 50ms timer to check whether the thread is still running, and running a GTK+ main loop until the thread has stopped, although I’m sure there are better ways to do this.
Here are some links to the code: