Qt(Python)开发中多线程通讯

前言

上文写了Python多线程中的通讯,这里我们说一下Qt使用PySide2开发时多线程之间的处理方式。

信号机制(推荐)

信号及处理类

main_signal_obj.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import threading

from PySide2.QtCore import QObject, Qt, Signal


class MainSignalObj(QObject):
installAppSignal = Signal(str)

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

# 创建信号
self.installAppSignal.connect(self.update_app, Qt.AutoConnection)

def update_app(self, app_path):
current_thread = threading.current_thread()
print(f"Worker thread: {current_thread.name} (ID: {current_thread.ident})")
print(f"Installing app: {app_path}")

全局变量

global_vars.py

1
2
3
# 共享模块
class GlobalVars:
mainSignalObj = None

初始化变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import sys
import threading
import time

from PySide2.QtCore import (QCoreApplication, QMetaObject, QObject, Qt, QThread, QUrl, Slot)
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from utils.main_signal_obj import MainSignalObj

def init_win():
QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
QGuiApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)

app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
qml_url = QUrl("qrc:/main.qml")
engine.load(qml_url)


mainSignalObj = MainSignalObj()
GlobalVars.mainSignalObj = mainSignalObj

if not engine.rootObjects():
sys.exit(-1)

sys.exit(app.exec_())


if __name__ == "__main__":
init_win()

分线程发送信号

1
2
3
4
5
6
7
8
def run_async_task():
current_thread = threading.current_thread()
print(f"Worker thread: {current_thread.name} (ID: {current_thread.ident})")
GlobalVars.mainSignalObj.installAppSignal.emit("app.exe")

# 创建并启动新线程
m_thread = threading.Thread(target=run_async_task)
m_thread.start()

信号连接类型

1
self.installAppSignal.connect(self.update_app, Qt.AutoConnection)

在 PySide2(以及 Qt 框架)中,信号关联的槽函数执行所在的线程并非由创建关联时所在的线程决定,而是由连接类型(connection type)来决定。

下面为你详细介绍不同连接类型下槽函数的执行线程:

  1. Qt.DirectConnection

    当使用 Qt.DirectConnection 连接类型时,槽函数会在发射信号的线程中立即执行。也就是说,槽函数的执行线程和发射信号的线程是相同的。

  2. Qt.QueuedConnection

    若使用 Qt.QueuedConnection 连接类型,槽函数会在接收对象所在的线程的事件循环中执行(也就是定义槽函数的那个对象创建时所在的线程)。

    通常,接收对象所在的线程就是创建该对象的线程。

    对于 GUI 相关的对象,一般是主线程。

  3. Qt.BlockingQueuedConnection

    Qt.BlockingQueuedConnectionQt.QueuedConnection 类似,槽函数会在接收对象所在的线程的事件循环中执行。

    不过,发射信号的线程会被阻塞,直到槽函数执行完毕。

  4. Qt.AutoConnection(默认连接类型)

    若使用 Qt.AutoConnection,Qt 会依据发射信号的线程和接收对象所在的线程来自动选择 Qt.DirectConnection 或者 Qt.QueuedConnection

    如果发射信号的线程和接收对象所在的线程相同,就使用 Qt.DirectConnection

    否则,使用 Qt.QueuedConnection

综上所述,信号关联的槽函数执行所在的线程由连接类型和接收对象所在的线程共同决定,而非创建关联时所在的线程。

QMetaObject.invokeMethod()

QMetaObject.invokeMethod() 是一个强大的工具,允许在不同线程中安全地调用方法。

不建议使用这种方法,使用字符串传递的方法名在代码排查的时候不方便。

语法

1
QMetaObject.invokeMethod(object, methodName, connectionType, *args)

参数

  • object: 要调用方法的对象。object 参数通常是 QObject 的子类实例。

  • methodName: 要调用的方法名,字符串形式。对应的方法要添加@Slot()注解

  • connectionType: 连接类型,通常是Qt.DirectConnectionQt.QueuedConnection

    • Qt.DirectConnection: 直接调用方法,通常在当前线程中执行。
    • Qt.QueuedConnection: 方法调用会被放入主线程的事件队列中,通常用于从其他线程调用主线程的方法,确保线程安全。
  • *args: 传递给方法的参数。

示例

被调用方法的类

1
2
3
4
5
6
7
8
9
10
class MainObj(QObject):
def __init__(self) -> None:
super().__init__()

@Slot()
def start_scanner(self):
print("开始扫描")
current_thread = threading.current_thread()
print(
f"Worker thread: {current_thread.name} (ID: {current_thread.ident})")

分线程运行的类

1
2
3
4
5
6
7
class BackgroundWorker(threading.Thread):
def __init__(self):
super().__init__()

def run(self):
print("后台线程运行")
start_ws_server()

全局变量

1
2
class GlobalVars:
main_obj = None

分线程调用主线程方法

1
2
3
from PySide2.QtCore import (QMetaObject)

QMetaObject.invokeMethod(GlobalVars.main_obj, "start_scanner", Qt.QueuedConnection)

主进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import sys
import threading
import time

from PySide2.QtCore import (QCoreApplication, QMetaObject, QObject, Qt, QThread, QUrl, Slot)
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine

def init_win():
QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
QGuiApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)

app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
qml_url = QUrl("qrc:/main.qml")
engine.load(qml_url)


mainObj = MainObj()
GlobalVars.main_obj = mainObj

# 后台任务
background_worker = BackgroundWorker()
background_worker.daemon = True
background_worker.start()

if not engine.rootObjects():
sys.exit(-1)

sys.exit(app.exec_())


if __name__ == "__main__":
init_win()

获取所在线程

1
2
3
4
import threading     

current_thread = threading.current_thread()
print(f"Worker thread: {current_thread.name} (ID: {current_thread.ident})")