community icon indicating copy to clipboard operation
community copied to clipboard

Feature Request: Add thread-aware signal/slot functionality

Open JulianOrteil opened this issue 3 years ago • 0 comments

Is your feature request related to a problem? Please describe. In PyQt/PySide, a signal/slot mechanism exists which allows developers to execute callbacks in other already-running threads. For example, if an event is posted from the main thread and the sender is bound to a callback in a class "owned" by another thread, that callback executes in that other thread, not the main.

This functionality technically exists via the Clock. schedule_* and @mainthread decorator: if an @mainthread decorated callback is scheduled from another thread via Clock.schedule_, then that callback executes in the main thread. Unfortunately, no such mechanism exists to support the reverse behavior (from the main thread to a worker thread) or a worker thread to another worker thread.

Describe the solution you'd like A mechanism like the signal/slot system described above would be ideal. However, because Kivy handles events differently than PyQt/PySide, what this ultimately looks like is not important--only that the functionality exists.

I believe the most straightforward approach would be to "copy" the implementation in PyQt. What this ultimately looks like I will leave to the implementer; however, PyQt follows this outline:

Note: there are two main approaches to this in PyQt: subclassing QThread (or threading.Thread in Kivy's case) and moving a QObject to a QThread.

Subclassing QThread:

  • QThread behaves like a normal threading.Thread
  • The developer is expected to implement the virtual run method to execute their code, like threading.Thread. This virtual function may need to be named differently for threading.Thread
  • Upon start, and behind the scenes, the QThread starts up its own event loop, independent from the main event loop
    • This secondary event loop allows the QThread to process events posted by pyqtSignals in both directions
  • For callbacks to execute property in a QThread, they must be decorated with a pyqtSlot. The intention and behavior of these is two-fold: filter signals and dispatch callbacks in the proper thread
    • These behave very similarly to @mainthread
  • Unlike threading.Thread, QThread does not shut down after the run method finishes executing. Only when explicitly told to do so via a quit method

Moving QObject:

  • QObject is essentially a normal class basing object. It holds special metadata, like the owner QThread, that would be useful, but isn't notable beyond that for this request
  • A normal QThread is created and the QObject is moved to the thread via a special moveToThread method
  • Upon start, the QThread, like above, spins up its own event loop to process pyqtSignals
  • Like above, callbacks, now implemented in the QObject, must be decorated with a pyqtSlot to execute properly
  • Note: I prefer this option because it adheres to the separation of concerns idea--the QThread dispatches events and the QObject implements callbacks

Describe alternatives you've considered In what would be a naive approach, in my opinion, a simple Queue consumer thread would likely be sufficient for this. This can be the "alternative" I've considered along with a twisted reactor. In this Discord post, I provided implementations in both Kivy (via the Properties and my own attempt with a Signal class) and PyQt to demonstrate this lack of functionality. After looking at that post, the real discussion I've had with others on the Discord server actually starts with this post.

JulianOrteil avatar Apr 24 '22 23:04 JulianOrteil