1

Simply put, I want to properly implement threading in a Python GTK application. This is in order to prevent UI freezing due to functions/code taking a long time to finish running. Hence, my approach was to move all code which took a long time to run into separate functions, and run them in their separate threads as needed. This however posed a problem when trying to run the functions in sequence.
For example, take a look at the following code:

class Main(Gtk.Window):
    def __init__(self):
        super().__init__()

        self.button = Gtk.Button(label='button')
        self.add(self.button)
        self.button.connect('clicked', self.main_function)


    def threaded_function(self):
        time.sleep(20)
        print('this is a threaded function')


    def first_normal_function(self):
        print('this is a normal function')


    def second_normal_function(self):
        print('this is a normal function')


    def main_function(self, widget):
        self.first_normal_function()
        self.threaded_function()
        self.second_normal_function()

Pressing the button starts main_function which then starts 3 functions in sequence. threaded_function represents a function which would take a long time to complete. Running this as is will freeze the UI. Hence it should be threaded as such:

...
...

def main_function(self, widget):
    self.first_normal_function()
    
    thread = threading.Thread(target=self.threaded_function)
    thread.daemon = True
    thread.start()
    
    self.second_normal_function()

What should happen is that the following first_normal_function should run, then threaded_function in a background thread - the UI should remain responsive as the background thread is working. Finally, second_normal_function should run, but only when threaded_function is finished.

The issue with this is that the functions will not run in sequence. The behaviour I am looking for could be achieved by using thread.join() however this freezes the UI.

So I ask, what's the proper way of doing this? This is a general case, however it concerns the general issue of having code which takes a long time to complete in a graphical application, while needing code to run sequentially. Qt deals with this by using signals, and having a QThread emit a finished signal. Does GTK have an equivalent?

I'm aware that this could be partially solved using Queue , with a put() and get() in relevant functions, however I don't understand how to get this to work if the main thread is calling anything other than functions.

EDIT: Given that it's possible to have threaded_function call second_normal_function using GLib.idle_add, let's take an example where in main_function, the second_normal_function call is replaced with a print statement, such that:

def main_function(self, widget):
    self.first_normal_function()
    
    thread = threading.Thread(target=self.threaded_function)
    thread.daemon = True
    thread.start()
    
    print('this comes after the thread is finished')
    ...
    ...
    ...
    #some more code here

With GLib.idle_add, the print statement and all the code afterwards would need to be moved into a separate function. Is it possible to avoid moving the print statement into its own function while maintaining sequentiality, such that the print statement remains where it is and still gets called after threaded_function is finished?

  • I think you can use GLib.tiemeout_add() to execute a slow task in another thread https://webreflection.github.io/gjs-documentation/GLib-2.0/GLib.timeout_add . About concatenate multiple operations, should be executed all together in the method called by timeout_add(). You ned return False to avoid calling the method repeatedly – Gonzalo Odiard Sep 09 '22 at 17:37
  • Would you be able to provide an example? – user_968563 Sep 09 '22 at 19:54
  • @GonzaloOdiard: not necessarily. Using [`GLib.timeout_add()`](https://lazka.github.io/pgi-docs/#GLib-2.0/functions.html#GLib.timeout_add) will still let the callback be called in the main loop, so if it's a long and complex operation, it will still freeze the UI as long as it takes to finish the function – nielsdg Sep 09 '22 at 20:39
  • Maybe this answer is more useful? https://stackoverflow.com/a/13371226/3969110 – Gonzalo Odiard Sep 09 '22 at 21:08
  • I believe this may be the answer. I'm somewhat struggling to implement it though. – user_968563 Sep 09 '22 at 21:43
  • Definitely also not ideal, but I have some time to write up an actual answer – nielsdg Sep 09 '22 at 21:57

1 Answers1

1

Your suggestion on how to do this was very close to the actual solution, but it's indeed not going to work.

In essence, what you'll indeed want to do, is to run the long-running function in a different thread. That'll mean you get 2 threads: one which is running the main event loop that (amongs other things) updates your UI, and another thread which does the long-running logic.

Of course, that bears the question: how do I notify the main thread that some work is done and I want it to react to that? For example, you might want to update the UI while (or after) some complex calculation is going on. For this, you can use GLib.idle_add() from within the other thread. That function takes a single callback as an argument, which it will run as soon as it can ("on idle").

So a possibility to use here, would be something like this:

class Main(Gtk.Window):

    def __init__(self):
        super().__init__()

        self.button = Gtk.Button(label='button')
        self.add(self.button)
        self.button.connect('clicked', self.main_function)

        thread = threading.Thread(target=self.threaded_function)
        thread.daemon = True
        thread.start()

    def threaded_function(self):
        # Really intensive stuff going on here
        sleep(20)

        # We're done, schedule "on_idle" to be called in the main thread
        GLib.idle_add(self.on_idle)

    # Note, this function will be run in the main loop thread, *not* in this one
    def on_idle(self):
        second_normal_function()
        return GLib.SOURCE_REMOVE # we only want to run once

    # ...

For more context, you might want to read the pygobject documentation on threading and concurrency

nielsdg
  • 2,318
  • 1
  • 13
  • 22
  • Thank you very much for your answer. So if I'm understanding this correctly, the threaded function needs to have a GLib.idle_add call, which then calls another function in the main thread once the threaded function is done doing its work. Given that, does this mean it's necessary to split all the stuff (in the main function) after the thread is called? – user_968563 Sep 09 '22 at 22:36
  • To add onto that, I modified the code such that: button press calls main_function -> main function runs first_normal_function -> threaded_function gets called (and is threaded) -> threaded_function waits 20 seconds and calls GLib.idle_add(self.second_normal_function) -> second_normal_function runs. I'm not sure what the purpose of GLib.SOURCE_REMOVE is though. – user_968563 Sep 09 '22 at 22:43
  • "does this mean it's necessary to split all the stuff" → not sure what you mean there. On the purpose of `GLib.SOURCE_REMOVE`: that's because `GLib.idle_add` requires a return value from the callback to know whether it should be called again or not. If you _would_ like to have this called over and over, you'd need to return `GLib.SOURCE_CONTINUE` – nielsdg Sep 10 '22 at 08:29
  • My bad, what I said was quite unclear. What I was trying to say is that if `main_function` starts `threaded_function` in a separate thread, when `threaded_function` runs `GLib.idle_add`, it needs to call another function, which thus means that whatever code would normally be run after `threaded_function` would need to be split off from `main_function` and into its own function. I will edit my original post to express this a bit more clearly – user_968563 Sep 10 '22 at 13:32
  • I see what you mean now; it should probably get its own question though, rather than tacking it onto this one ;-) In any case: the answer is no. You're dealing with asynchronous programming at this point, which means you can't do something blocking in a function, because it will block your UI as well – nielsdg Sep 10 '22 at 14:47
  • I see, many thanks! I might post a separate question regarding this issue as well. – user_968563 Sep 10 '22 at 16:30