PySide6从0开始学习的笔记(二十三)使用QRunnable在线程池中执行临时任务

简要介绍:

  • QRunnable是轻量级的任务载体
  1. 核心定位:只封装「任务逻辑」,不具备线程能力。 QRunnable 的唯一核心作用是「打包要在子线程中执行的业务代码」,它只回答「要做什么」(即 run() 方法中封装的逻辑),本身不是线程,无法独立运行,必须依赖 QThreadPool 才能被调度执行。

  2. 「轻量级」的核心体现

    • 结构极简:仅需重写一个 run() 方法即可使用,无其他强制重写的抽象方法;

    • 无额外开销:不继承 QObject,没有 Qt 信号槽、事件循环等附加机制,创建和销毁的资源消耗极低;

    • 无需手动管理生命周期:默认 autoDelete=True,任务执行完毕后,QThreadPool 会自动销毁其实例,无需手动绑定 finished 信号和 deleteLater(),避免内存泄露且减少代码工作量。

    • 对比凸显轻量

      QThread 相比,QThread 既是「线程载体」又是「生命周期管理者」,自带线程启停、事件循环等复杂逻辑;而 QRunnable 只专注于「任务本身」,把线程的创建、调度、复用全交给 QThreadPool,更适合执行简单、短期的临时任务。

  • QThreadPool.globalInstance()是获取全局线程池实例的语句:
  1. 所属框架与核心类: 该语句依赖 Qt 框架的QThreadPool类,该类是 Qt 提供的线程池管理工具,用于统一管理、复用线程,避免频繁创建 / 销毁线程带来的性能开销,是 Qt 中实现多线程任务(尤其是短小、大量的任务)的常用方案。

  2. globalInstance() 方法的核心作用: globalInstance()QThreadPool静态方法(static method) ,它的核心功能不是 创建新的线程池对象,而是获取 Qt 框架内置的、整个应用程序级别的全局唯一线程池实例

    • 这个实例是 Qt 在应用程序启动时自动初始化的,全程单例(唯一);
    • 无需开发者手动创建QThreadPool对象,直接通过该方法获取实例后即可随时复用全局线程池,简化多线程代码实现。
  3. 语句的执行结果与后续用途: 语句执行后,随时可以引用(指向)这个全局唯一的 QThreadPool 实例(而非创建新实例),后续开发者可以通过实例调用QThreadPool的成员方法(如start()提交任务、setMaxThreadCount()设置最大线程数等),实现多线程任务的提交与管理。

  4. **补充核心特性:**全局线程池的生命周期与整个 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())

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

相关推荐
CCPC不拿奖不改名18 小时前
网络与API:HTTP基础+面试习题
网络·python·网络协议·学习·http·面试·职场和发展
MistaCloud18 小时前
Pytorch深入浅出(十五)之GPU加速与设备管理
人工智能·pytorch·python·深度学习
三档程序员18 小时前
适配龙芯笔记之 libthriftnb.so 链接libevent失败
笔记
声网18 小时前
如何用 Fun-ASR-Nano 微调一个「听懂行话」的语音模型?丨Voice Agent 学习笔记
笔记·学习·xcode
Aurora-Borealis.18 小时前
Day31 函数专题2
python
蓝冰凌18 小时前
python版本管理工具
python
Data_agent18 小时前
Pantherbuy模式淘宝 / 1688 代购系统(欧美市场)搭建指南
大数据·python·产品经理
weixin_4624462318 小时前
Python Flask静态文件服务器:支持自动JSON扩展名补全的智能文件服务
服务器·python·flask
杰瑞不懂代码18 小时前
playwright 基础入门教程,更便捷的数据获取
python·网络爬虫·playwright·自动化处理