简要介绍:
QRunnable是轻量级的任务载体
-
核心定位:只封装「任务逻辑」,不具备线程能力。
QRunnable的唯一核心作用是「打包要在子线程中执行的业务代码」,它只回答「要做什么」(即run()方法中封装的逻辑),本身不是线程,无法独立运行,必须依赖QThreadPool才能被调度执行。 -
「轻量级」的核心体现
-
结构极简:仅需重写一个
run()方法即可使用,无其他强制重写的抽象方法; -
无额外开销:不继承
QObject,没有 Qt 信号槽、事件循环等附加机制,创建和销毁的资源消耗极低; -
无需手动管理生命周期:默认
autoDelete=True,任务执行完毕后,QThreadPool会自动销毁其实例,无需手动绑定finished信号和deleteLater(),避免内存泄露且减少代码工作量。 -
对比凸显轻量
与
QThread相比,QThread既是「线程载体」又是「生命周期管理者」,自带线程启停、事件循环等复杂逻辑;而QRunnable只专注于「任务本身」,把线程的创建、调度、复用全交给QThreadPool,更适合执行简单、短期的临时任务。
-
QThreadPool.globalInstance()是获取全局线程池实例的语句:
-
所属框架与核心类: 该语句依赖 Qt 框架的
QThreadPool类,该类是 Qt 提供的线程池管理工具,用于统一管理、复用线程,避免频繁创建 / 销毁线程带来的性能开销,是 Qt 中实现多线程任务(尤其是短小、大量的任务)的常用方案。 -
globalInstance()方法的核心作用:globalInstance()是QThreadPool的静态方法(static method) ,它的核心功能不是 创建新的线程池对象,而是获取 Qt 框架内置的、整个应用程序级别的全局唯一线程池实例。- 这个实例是 Qt 在应用程序启动时自动初始化的,全程单例(唯一);
- 无需开发者手动创建
QThreadPool对象,直接通过该方法获取实例后即可随时复用全局线程池,简化多线程代码实现。
-
语句的执行结果与后续用途: 语句执行后,随时可以引用(指向)这个全局唯一的 QThreadPool 实例(而非创建新实例),后续开发者可以通过
实例调用QThreadPool的成员方法(如start()提交任务、setMaxThreadCount()设置最大线程数等),实现多线程任务的提交与管理。 -
**补充核心特性:**全局线程池的生命周期与整个 Qt 应用程序绑定,应用程序退出时自动销毁,无需开发者手动释放资源,降低了多线程开发的资源管理成本。
使用方法:
二者结合,非常适合执行简单、短期的任务,Qt 在应用程序启动初始化一个线程池实例:QThreadPool.globalInstance(),然后创建QRunnable实例,再使用QThreadPool.globalInstance()实例的start()方法启动QRunnable实例,与QThread 线程相比,代码简洁且无需关心线程销毁(线程池自动管理)。
- 最简化代码:
python
import sys
import time
from PySide6.QtCore import QMutex, QRunnable, QThreadPool, QMutexLocker
from PySide6.QtWidgets import QApplication, QPushButton, QWidget, QVBoxLayout
# 1. 包装QRunnable任务(极简包装,无需复杂继承逻辑)
class Runnable(QRunnable):
def __init__(self, buf_lock):
super().__init__()
self.buf_lock = buf_lock
# QRunnable接口的实现:run()方法
def run(self):
with QMutexLocker(self.buf_lock):
# 临界区:获取互斥锁后执行业务函数
print("临时线程:获取锁,执行操作")
# 模拟耗时操作
time.sleep(5) # 耗时操作在互斥锁的临界区内执行
print("临时任务执行完毕")
# 2. 按钮点击触发函数(创建任务 + 提交到线程池,无需手动启动线程)
def on_button_clicked(lock):
"""点击按钮触发,快速提交临时任务到线程池"""
runnable = Runnable(lock)
thread_pool.start(runnable) # 线程池自动分配线程执行任务
if __name__ == '__main__':
app = QApplication(sys.argv)
lock = QMutex() # 全局互斥锁
thread_pool = QThreadPool.globalInstance() # 获取全局线程池(无需手动创建)
# 初始化按钮,绑定点击事件
btn = QPushButton("创建临时线程执行 1 操作")
btn.clicked.connect(lambda: on_button_clicked(lock))
btn.show()
sys.exit(app.exec())
- 将任务封装成函数:
python
import sys
import time
from PySide6.QtCore import QMutex, QRunnable, QThreadPool, QMutexLocker
from PySide6.QtWidgets import QApplication, QPushButton, QWidget, QVBoxLayout
# 1. 定义简单的任务执行函数(无需创建QObject/QThread子类,仅处理核心逻辑)
def task1():
print("临时线程1:获取锁,执行 1 操作")
def task2():
print("临时线程2:获取锁,执行 2 操作")
# 2. 包装QRunnable任务(极简包装,无需复杂继承逻辑)
class Runnable(QRunnable):
def __init__(self, buf_lock, task):
super().__init__()
self.buf_lock = buf_lock
self.task = task
# QRunnable接口的实现:run()方法
def run(self):
with QMutexLocker(self.buf_lock):
# 临界区:获取互斥锁后执行业务函数
self.task()
# 模拟耗时操作
# time.sleep(5) # 耗时操作在互斥锁的临界区内执行 or:
time.sleep(5) # 耗时操作在互斥锁的临界区外执行,可以体会一下二者的区别
print(f"临时任务:{self.task.__name__}执行完毕")
# 3. 按钮点击触发函数(创建任务 + 提交到线程池,无需手动启动线程)
def on_button_clicked(lock, task):
"""点击按钮触发,快速提交临时任务到线程池"""
runnable = Runnable(lock, task)
thread_pool.start(runnable) # 线程池自动分配线程执行任务
if __name__ == '__main__':
app = QApplication(sys.argv)
lock = QMutex() # 全局互斥锁
thread_pool = QThreadPool.globalInstance() # 获取全局线程池(无需手动创建)
# 初始化按钮,绑定点击事件
btn1 = QPushButton("创建临时线程执行 1 操作")
btn1.clicked.connect(lambda: on_button_clicked(lock, task1))
btn2 = QPushButton("创建临时线程执行 2 操作")
btn2.clicked.connect(lambda: on_button_clicked(lock, task2))
form = QWidget()
layout = QVBoxLayout(form)
layout.addWidget(btn1)
layout.addWidget(btn2)
form.setLayout(layout)
form.show()
sys.exit(app.exec())
- 将工作的对象封装为类:
python
import sys
import time
from PySide6.QtCore import QMutex, QRunnable, QThreadPool, QMutexLocker, QObject
from PySide6.QtWidgets import QApplication, QPushButton, QWidget, QVBoxLayout
# 工作的对象
class WorkObject(QObject):
def __init__(self, lock):
super().__init__()
self.lock = lock
# 按钮点击触发函数(创建任务 + 提交到线程池,无需手动启动线程)
def on_button_clicked(self):
"""点击按钮触发,快速提交临时任务到线程池"""
runnable = Runnable(self.lock)
thread_pool.start(runnable) # 线程池自动分配线程执行任务
# 包装QRunnable任务
class Runnable(QRunnable):
def __init__(self, buf_lock):
super().__init__()
self.buf_lock = buf_lock
# QRunnable接口的实现:run()方法
def run(self):
with QMutexLocker(self.buf_lock):
# 临界区:获取互斥锁后执行业务函数
print(f"临时线程:获取锁,执行操作")
# 模拟耗时操作
# time.sleep(5) # 耗时操作在互斥锁的临界区内执行 or:
time.sleep(5) # 耗时操作在互斥锁的临界区外执行
print(f"临时任务执行完毕")
if __name__ == '__main__':
app = QApplication(sys.argv)
lock = QMutex() # 全局互斥锁
thread_pool = QThreadPool.globalInstance() # 获取全局线程池(无需手动创建)
obj_thread = WorkObject(lock) # 创建对象线程对象
# 初始化按钮,绑定点击事件
btn1 = QPushButton("创建临时线程执行操作")
btn1.clicked.connect(lambda: obj_thread.on_button_clicked())
btn1.show()
sys.exit(app.exec())
上面的代码,每次点击按钮,都定义了一个QRunnable,并在线程池中自动运行,运行完成后自动销毁。另外,代码中模拟的耗时操作time.sleep(5)放在互斥锁的临界区内还是外执行,区别还是很明显的。
工程意义:
比如,一个相机采图项目,当从相机中获取到了像素数据后,需要进行保存,就可以把保存的操作封装为QRunnable,并且只在互斥锁中执行内存拷贝,写硬盘的操作不影响相机的采图线程:
python
import sys
import time
from copy import deepcopy
from PySide6.QtCore import QMutex, QRunnable, QThreadPool, QMutexLocker, QThread, Signal
from PySide6.QtWidgets import QApplication, QPushButton, QWidget, QVBoxLayout, QLabel
def save_bmp():
global frame_data
time.sleep(0.1) # 模拟复制像素数据到内存的耗时过程
print(f"获取到了\"{frame_data}\"bmp数据到内存")
def save_jpg():
global frame_data
time.sleep(0.1) # 模拟复制像素数据到内存的耗时过程
print(f"获取到了\"{frame_data}\"jpg数据到内存")
# 包装QRunnable任务
class Runnable(QRunnable):
def __init__(self, buf_lock, task):
super().__init__()
self.buf_lock = buf_lock # 共享的互斥锁对象
self.task = task # 任务函数
# QRunnable接口的实现:run()方法
def run(self):
with QMutexLocker(self.buf_lock): # 使用QMutexLocker自动获取/释放互斥锁
# 临界区:获取互斥锁后执行业务函数
self.task() # 执行任务函数
print(f"任务:{self.task.__name__}已释放互斥锁,开始在硬盘写数据")
time.sleep(2) # 模拟保存文件的耗时过程
print(f"任务:{self.task.__name__}硬盘写数据完毕")
# 3. 按钮点击触发函数(创建任务 + 提交到线程池,无需手动启动线程)
def on_button_clicked(lock, task):
"""点击按钮触发,快速提交临时任务到线程池"""
runnable = Runnable(lock, task) # 创建QRunnable任务对象
thread_pool.start(runnable) # 线程池自动分配线程执行任务
# 4. 模拟相机的采样
class CameraQthread(QThread):
outFrame = Signal(int) # 模拟相机输出的帧数据
def __init__(self, lock):
super().__init__()
self.lock = lock # 共享的互斥锁对象
self.frame_num = 0 # 用帧数计数器模拟相机采样得到的帧数据
def run(self):
global frame_data
while True:
with QMutexLocker(self.lock):
time.sleep(0.2) # 模拟相机采样耗时
self.frame_num += 1 # 帧数计数,模拟采样到的数据
frame_data = deepcopy(self.frame_num) # 模拟采样到的数据拷贝到全局的帧数据,深度拷贝,防止数据被修改
self.outFrame.emit(self.frame_num) # 发送信号,模拟界面显示相机采样到的图像
if __name__ == '__main__':
app = QApplication(sys.argv)
lock = QMutex() # 全局互斥锁
thread_pool = QThreadPool.globalInstance() # 获取全局线程池(无需手动创建)
frame_data = 0 # 全局帧数据,模拟相机采样得到的数据
camera = CameraQthread(lock) # 创建相机线程对象
camera.start()
# 初始化按钮,绑定点击事件
btn1 = QPushButton("保存为bmp格式")
btn1.clicked.connect(lambda: on_button_clicked(lock, save_bmp))
btn2 = QPushButton("保存为jpg格式")
btn2.clicked.connect(lambda: on_button_clicked(lock, save_jpg))
# 添加一个标签,模拟相机的采样
label = QLabel("相机帧数据")
camera.outFrame.connect(lambda num:label.setText(f"相机帧数据:{num}"))
# 初始化窗口
form = QWidget()
layout = QVBoxLayout(form)
layout.addWidget(label)
layout.addWidget(btn1)
layout.addWidget(btn2)
form.setLayout(layout)
form.show()
sys.exit(app.exec())

从运行截图中可以看到,由于将写硬盘这种耗时工作放在了互斥锁的临界区外,互斥锁只在内存拷贝阶段对相机的采图有影响,而保存到硬盘的操作对相机的采图没有任何影响。