Connecting to "close-request" from thread hangs application

I’m trying to run the logic of my app in a separate thread. I followed the instructions from Threads & Concurrency - PyGObject by using GLib.idle_add where needed. However, my app freezes upon closing the window. The only thing I do from the thread is to connect to the “close-request” signal. The callback is executed and the thread terminates cleanly, so I don’t understand what is blocking afterward.

import sys
import threading

import gi

gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
gi.require_version("WebKit", "6.0")
from gi.repository import Gtk, Adw, GLib


class MainWindow(Gtk.ApplicationWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.set_default_size(640, 480)
        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.set_child(self.box)


class MyApp(Adw.Application):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.connect("activate", self.on_activate)

    def on_activate(self, app):
        self.win = MainWindow(application=app)
        self.win.present()

        thread = threading.Thread(target=main, args=[app])
        thread.daemon = True
        thread.start()


def main(app: MyApp):
    close_evt = threading.Event()
    GLib.idle_add(
        app.win.connect,
        "close-request",
        lambda *kargs: close_evt.set(),
    )
    close_evt.wait()
    print("closing")


app = MyApp(application_id="com.example.myapp")
app.run(sys.argv)

The problem is that in using connect as an idle handler, you’re returning the signal handler ID, a value equivalent to True (aka GLib.SOURCE_CONTINUE) and so you’re calling that function continuously. Just use a new function that returns GLib.SOURCE_REMOVE (technically it’ll also work if you let it return None).

I don’t see any obvious problems with your example. You’ve got the basic idea correct: by connecting to the signal in an idle callback that runs on the main thread, you avoid threadsafety problems. Not doing that is the common threading mistake. And then you cancel the python event on the main thread, which should end the wait on the secondary thread. It looks good to me. I’m not sure why it doesn’t work. We must be missing something.

Good catch!

Do I really need to call connect through idle_add though?
The doc page seems to implies otherwise:

In which context the object is created or where connect() is called from doesn’t matter

Thanks!

Too bad Discourse didn’t show me your post before I responded, considering you posted a full 11 minutes before I did. Oh well.

Good point. You just have to be certain the object is still valid at the time you connect to its signal. As long as you’re certain it’s still alive, then it’s OK to connect from whatever thread you want. The thread you connect from has no impact on which thread the signal will be emitted from. In this case, app is a global object and you know it’s guaranteed to be valid for the lifetime of your thread, so you don’t need to use the idle callback after all. However, if you were doing anything more complex than just connecting a signal handler, you would still need it.

1 Like

All right! Thanks you both for your help and clarifications.