Feature Request: Add thread-aware signal/slot functionality
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
runmethod to execute their code, likethreading.Thread. This virtual function may need to be named differently forthreading.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
pyqtSignalsin both directions
- This secondary event loop allows the QThread to process events posted by
- 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
- These behave very similarly to
- Unlike
threading.Thread, QThread does not shut down after therunmethod finishes executing. Only when explicitly told to do so via aquitmethod
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
moveToThreadmethod - 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
pyqtSlotto 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.