Gtk threading problem with GLib.idle_add

I am trying to run multiple background threads in a gtk app.

I followed this tutorial to implement thread safe gtk application gtk-threads. However, when I implement the threading using GLib.idle_add it seems to block the main loop.

main.py

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib

from threads import run_in_thread_gtk
import random

window = Gtk.Window()

main_box = Gtk.Box()

list_box = Gtk.ListBox()

button = Gtk.Button(label='update')

button2 = Gtk.Button(label='test button')

main_box.pack_start(list_box, True, True, 0)
main_box.pack_start(button, False, False, 0)
main_box.pack_start(button2, False, False, 0)

window.add(main_box)
window.show_all()

def update_rows(*args, **kwargs):
    for child in list_box.get_children():
        list_box.remove(child)

    for i in range(10):
        list_box.add(Gtk.Label(label=f"Row {random.random()}"))

    from time import sleep

    sleep(2)


    list_box.show_all()

    return False

button.connect('clicked', run_in_thread_gtk, update_rows)

Gtk.main()

thread.py

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib

from threading import Thread, BoundedSemaphore

semaphore = BoundedSemaphore(10)
def run_in_thread_gtk(widget, func, *args, **kwargs):
    global semaphore

    thread = Thread(target=thread_func_wrapper, args=(func, semaphore,))
    thread.start()

    return True

def thread_func_wrapper(func, semaphore):
    with semaphore:
        GLib.idle_add(func) # I use GLib.idle_add here 

Use the previous code it seems that the GLib.idle_add blocks the main loop and the UI freeze. However, when I change the code the following code it works fine.

main.py

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib

from threads import run_in_thread_gtk
import random

window = Gtk.Window()

main_box = Gtk.Box()

list_box = Gtk.ListBox()

button = Gtk.Button(label='update')

button2 = Gtk.Button(label='test button')

main_box.pack_start(list_box, True, True, 0)
main_box.pack_start(button, False, False, 0)
main_box.pack_start(button2, False, False, 0)

window.add(main_box)
window.show_all()

def update_rows(*args, **kwargs):
    for child in list_box.get_children():
        GLib.idle_add(list_box.remove, child)

    for i in range(10):
        GLib.idle_add(list_box.add, Gtk.Label(label=f"ROW {random.random()}"))

    from time import sleep

    sleep(2)

    GLib.idle_add(list_box.show_all)

    return False

button.connect('clicked', run_in_thread_gtk, update_rows)

Gtk.main()

thread.py

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib

from threading import Thread, BoundedSemaphore

semaphore = BoundedSemaphore(10)
def run_in_thread_gtk(widget, func, *args, **kwargs):
    global semaphore

    thread = Thread(target=thread_func_wrapper, args=(func, semaphore,))
    thread.start()

    return True

def thread_func_wrapper(func, semaphore):
    with semaphore:
        func() # No GLib.idle_add

The difference between the codes is the place where I used GLib.idle_add.

GLib.idle_add causes a function to be invoked back in the GTK main thread. The update_rows function statement in the first program is running on the main thread so it blocks the event loop with sleep.

Your second example is actually calling update_rows in another thread, but in GTK you should never try to do that. GTK is not thread safe, it is only safe to call GTK functions from the main thread.

Note in python you must also content with global interpreter lock which means you must only use certain functions that escape the interpreter to perform threaded tasks, or else you can still end up blocking the main thread.

No in the first example GLib.idle_add is actually running in a separate thread not the main thread. I don’t know if you see something different in my code that I don’t understand. But the first example is exactly the same as the second example with the difference in the way I used GLib.idle_add.

Based on this tutorial, it is save to use gtk in mutlithread program and access the gtk widgets and modify it.

If you look carefully in the tutorial you would see that it is similar to my first example.

Thank you for your help.

Your program and the example you link have a fundamental difference: time.sleep() is run in the GTK (main) thread with yours, and in the worker thread in the tutorial.

I guess you didn’t understand what GLib.idle_add() actually does: it registers a function to be called on the main thread whenever the GTK main loop has a chance. What makes it “thread safe” is that it is safe to call GLib.idle_add() from any thread.

You can see in the tutorial you linked that the time.sleep() call is next to the GLib.idle_add() one, and it’s in the worker thread (example_target). The function that actually interacts with GTK is update_progress(), which is the one dispatched through GLib.idle_add() (and it doesn’t sleep()).

Think of it like this, GLib.idle_add is used to execute a function in the main thread.

In the tutorial, the following line inside the worker thread

GLib.idle_add(update_progess, i)

adds update_progress(i) to be run inside the main thread. It runs when whatever was running inside the main thread has finished. update_progress(i) blocks the main thread as long as it is being executed. But it executes so quickly, you don’t even notice it.

Note: You shouldn’t be using threads directly anyway. Try using a thread pool or Gio.Task instead.

What I do is that I have created a generic BackgroundTask class and I use that. It uses Gio.Task behind the scenes but is very simple to use. It is free software. So, you can copy it to your project if you want.

Its usage is like this;

import time

# The actual stuff you want to run in the background
def sleep_and_string(n_seconds: int) -> str:
    time.sleep(n_seconds)
    return "Slept for " + n_seconds + " seconds"


# This will be called when the background task finishes running.
# You can get the return value and error status via task.finish().
# You can also update the GUI here.
def on_sleep_finished(task):
    try:
        retval = task.finish()
        print(retval)
    except Exception as err:
       print("Unhandled exception occured!", err.message)


# Create a background task
sleep_task = BackgroundTask(lambda: sleep_and_string(10),
                            lambda: on_sleep_finished(task))

# Start the task
sleep_task.start()
# Once finished, the task can be started again if required.

Edit: Fixed some syntax errors in the code.

1 Like

Cool, I really appreciate it. Thank you very much.

By the way is it possible to use something similar to BoundedSemaphore to limit the number of threads by the Gio.Task?

Sorry, I don’t know about that.

I learned the bare minimum about threading/parallel programming in order to prevent my app from freezing during a long running operation.

You can do rate limiting easier by creating your own thread pool. Do not try to do locking if you are using Gio.Task.run_in_thread, that can block any other I/O tasks performed by gio.

You may find these three tutorials relevant: