How to make responsive GTK+ applications

Introduction


This weekend I've made a GTK+ application, I've done my best to make it responsive by applying my old Android development experience.

Android make it clear that you should not block UI thread (main thread) not non-UI tasks like:

  • disk IO (read a file)
  • network IO (request remote API)
  • internal SQLite database
  • intensive computations
 Let me quote:

"You should not perform the work on the UI thread, but instead create a worker thread and do most of the work there."

private class MyTask extends AsyncTask... {
    protected Long doInBackground(URL... urls) {
    // worker thread
    }
    protected void onProgressUpdate(Integer... progress) {
    // ui thread
    }
    protected void onPostExecute(Long result) {
    // ui thread
    }
}

GTK+

GTK+ is not threadsafe, in the sense all calls to GTK+ should be from a single thread that is the main thread or the UI thread, which seems similar to Android.

We have a class that loads the glade XML files and show the main window, in the constructor `__init__()` function we launch a worker thread and a queue as you can see here

self.keep_running = True
self.queue = Queue()
thread = Thread(target=self.worker_loop)
thread.daemon = True
thread.start()

Being a daemon thread mean, if the main thread ends, there is no need to wait for worker threads, in other words if the main window is closed and GTK main loop quit was triggered before the background task (ex. search query) is done, close the app without waiting for the background task to finish. Use it depending on application and how critical is the background task.

The thread target is a loop that pulls a task from the queue and execute it in the worker thread without blocking UI thread

    def worker_loop(self):
        while self.keep_running:
            try: a = self.queue.get(timeout=10)
            except Empty: continue
            cb_name, kwargs = a
            cb=getattr(self, cb_name)
            if not cb:
                self.queue.task_done()
                continue
            try: cb(**kwargs)
            except Exception as e: 
                logger.error("ERROR: %r", e)
            self.queue.task_done()
        logger.info("worker thread exited")

Because of timeout=10, every 10 seconds it would check if the flag "self.keep_running" is still on. Another different way is to have a special signal/trigger to quit the loop like pushing None to the queue.

We are going to push the task in the form of a method name and a dictionary of named parameters.

// call self.goto_page(page_id=0) in the background
self.queue.put(('goto_page', {'page_id': 0},))

Task method can do a blocking task (SQL query, network request, ...etc.) in a different thread, while UI thread works fine. When the task want to update the UI it can use GLib.idle_add which would execute the passed callback (which can be a method or on-the-fly lambda) in the UI thread

GLib.idle_add(lambda: self.body.get_buffer().set_text(text))


And that's it

Comments

Popular posts from this blog

How to defuse XZ Backdoor (or alike) in SSH Daemon

creating CSS3 animations

CSS3 transformation with Rotating gears example