Home > Mobile >  Python Qt - do signals need to be created in the worker thread?
Python Qt - do signals need to be created in the worker thread?

Time:03-09

I am using Python Qt (PySide2) which has a GUI in the main thread and a library that does IO, data-crunching, etc in a second thread.

I use signals & slots to update the GUI. In the examples I have seen on SO, the signal is always created in the worker (non-GUI) thread. Is it necessary to do this?

Reason: my library can be used with a GUI or could be used in another Python script. So, it might output data to the GUI or maybe to console/log file. To make the code in the library generic, I thought that whatever calls the library can register a callback. That callback can be to emit to Qt or output to file, etc.

Here's an example where the Signal is created in the GUI thread. It works, but could it cause thread-related issues?

import threading
import time
import sys
from PySide2.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QApplication
from PySide2 import QtCore

class aio_connection(QtCore.QObject):
    data_recvd = QtCore.Signal(object) 

class TextEditDemo(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setWindowTitle("TEST")
        self.resize(600,540)

        self.textEdit = QTextEdit()
        self.btnPress1 = QPushButton("Run")

        layout = QVBoxLayout()
        layout.addWidget(self.textEdit)
        layout.addWidget(self.btnPress1)
        self.setLayout(layout)

        self.btnPress1.clicked.connect(self.btnPress1_Clicked)

        self.aio_conn = aio_connection()    # Signal is created in main (GUI) thread

        # Connect signals (data_recvd) and slots (on_data_ready)
        self.aio_conn.data_recvd.connect(self.on_data_ready)

    def btnPress1_Clicked(self):
        threading.Thread(target=bg_task, args=(self.cb,)).start() 

    @QtCore.Slot()
    def on_data_ready(self, msg):
        self.textEdit.append(msg)

    def cb(self, info):
        self.aio_conn.data_recvd.emit(info)

# Simulate the library that runs in a second thread
def bg_task(callback):
    for i in range(100):
        callback(str(i))
        time.sleep(0.1)

def main():
    app = QApplication(sys.argv)
    win = TextEditDemo()
    win.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

CodePudding user response:

By default, signal connections are made using the AutoConnection type. This means the eventual type of connection will be automatically made at runtime, when the signal is actually emitted. If the sender and receiver are in different threads, Qt will use a queued connection, which will post an event to the event-queue of the receiving thread. Thus, when control returns to the receiver, the event will be processed and any connected slots will be called at that time.

This is an important consideration when updating GUI elements from worker threads, because Qt does not support GUI-related operations of any kind outside the main thread. However, so long as you always use signals to communicate between threads using the default connection-type, you should not have any problems - Qt will automatically guarantee they are done in a thread-safe way.

Below is a version of your scripts that verifies everything is working as desired. When I run it, I get the following output, which shows the current thread-id within each function call:

main: 4973
bg_task: 4976
cb: 4976
on_data_ready: 4973
cb: 4976
on_data_ready: 4973
cb: 4976
...
import threading, time, sys
from PySide2.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QApplication
from PySide2 import QtCore

class aio_connection(QtCore.QObject):
    data_recvd = QtCore.Signal(object)

class TextEditDemo(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("TEST")
        self.resize(600,540)
        self.textEdit = QTextEdit()
        self.btnPress1 = QPushButton("Run")
        layout = QVBoxLayout()
        layout.addWidget(self.textEdit)
        layout.addWidget(self.btnPress1)
        self.setLayout(layout)
        self.btnPress1.clicked.connect(self.btnPress1_Clicked)
        self.aio_conn = aio_connection()
        self.aio_conn.data_recvd.connect(self.on_data_ready)

    def btnPress1_Clicked(self):
        threading.Thread(target=bg_task, args=(self.cb,)).start()

    @QtCore.Slot()
    def on_data_ready(self, msg):
        print('on_data_ready:', threading.get_native_id())
        self.textEdit.append(msg)

    def cb(self, info):
        print('cb:', threading.get_native_id())
        self.aio_conn.data_recvd.emit(info)

def bg_task(callback):
    print('bg_task:', threading.get_native_id())
    for i in range(5):
        callback(str(i))
        time.sleep(0.1)

def main():
    print('main:', threading.get_native_id())
    app = QApplication(sys.argv)
    win = TextEditDemo()
    win.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()
  • Related