前言
PySide6/PyQT多线程同时访问同一个共享资源或对象,程序可能会出现预期之外的结果。所以需要考虑线程安全问题。
使用PySide6/PyQT开发GUI应用程序,在多个线程同时访问同一个共享对象时候,如果没有进行同步处理那就可能会导致数据不一致或者一些意料之外的问题发生。
所以使用多线程就得考虑线程安全,而线程安全最简单的处理方法就是 线程锁 。当然,使用信号槽机制(Siganl、Slot)也可以完美解决这个问题。
本篇文章使用 QMutex 来解决多线程占用共享资源的问题。
至于 信号槽机制(Siganl、Slot)在另外一篇文章有介绍,所以这里不做过多赘述。
知识点📖📖
本文用到的几个PySide6的知识点及链接。
作用 | 链接 |
---|---|
创建新线程 | QThread |
线程同步机制,用于协调多个线程之间对共享资源的访问 | QMutex |
对象间通信的机制,允许对象发送和接收信号 | Signal |
用于响应Signal信号的方法 | Slot |
实现
注意事项
当然,如果可以解决线程安全的问题,那就当这里没说。
关于保证线程安全,可以遵循以下几个原则:
- 尽量不要在多个线程中访问和修改同一个对象;
- 如果必须要访问和修改同一个对象,需要使用线程同步机制,例如信号槽、互斥锁、条件变量等;
- 避免使用共享状态,例如全局变量,尽量将状态封装在对象内部,并使用线程安全的方式访问和修改状态;
- 不要使用原生的线程库,选择使用 Qt 提供的 QThread、QThreadPool 等线程库。
上面这几点其实差不多一个意思,有些概念就行。
示例代码
线程不安全代码
代码释义
Worker类
- 继承了QThread类,并创建 信号valueChanged;
- 接收两个参数,一个为name,一个为MainWindow类实例对象本身;
- 循环5次,每次为 MainWindow实例的count累加1;
- 并使用valueChanged 信号将执行结果和执行次数发送出去;
MainWindow类
- 继承了QWidget,实现了包含一个按钮和一个标签的窗口;
- setup_thread函数为窗口布局;
- setup_thread函数 实例化两个Worker类,并将它们的信号连接到Slot槽函数 thread_finished ;
- 按钮绑定了 start_threads函数 ;
- thread_finished函数为Slot槽函数,用于接收 Worker 的信号发送的结果。
代码中两个线程同时访问了 self.count,结合本篇文章标题,如果程序没出错,那在这里一定会出错。看看下面的运行结果。
# -*- coding: utf-8 -*-
import sys
import time
from PySide6.QtCore import (QThread, Signal, Slot)
from PySide6.QtWidgets import (QApplication, QLabel, QPushButton, QVBoxLayout, QWidget)
class Worker(QThread):
valueChanged = Signal(tuple)
def __init__(self, name, main_window):
super().__init__()
self.name = name
self.main_window = main_window
def run(self):
for i in range(5):
self.main_window.count += 1
time.sleep(0.1)
self.valueChanged.emit((self.name, self.main_window.count))
class MainWindow(QWidget):
def __init__(self):
self.count = int()
super().__init__()
self.setup_ui()
self.setup_thread()
def setup_ui(self):
layout = QVBoxLayout()
self.label = QLabel("Count: 0", self)
self.btn_start = QPushButton("Start", self)
self.btn_start.clicked.connect(self.start_threads)
layout.addWidget(self.label)
layout.addWidget(self.btn_start)
self.setLayout(layout)
self.setGeometry(300, 300, 250, 150)
self.show()
def setup_thread(self):
self.worker1 = Worker('thread_1', self)
self.worker2 = Worker('thread_2', self)
self.worker1.valueChanged.connect(self.thread_finished)
self.worker2.valueChanged.connect(self.thread_finished)
def start_threads(self):
self.worker1.start()
self.worker2.start()
@Slot(tuple)
def thread_finished(self, value):
print(str(value))
self.label.setText(f"{str(value)}")
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec())
运行结果
代码运行效果如下图所示:
- 如果程序没有出错,那应该是按照顺序打印 1~10;
- 但这个并不是按照顺序的,那就是说明它们在争夺 self.count时候出现了岔子;
- 互相争夺共享资源,出现了问题。看后面来修正这个问题。
QMutex 线程安全
线程同步机制,用于协调多个线程之间对共享资源的访问。
PySide6中可以通过QMutex实现线程锁。QMutex是一个用于同步线程执行的互斥锁,可以保护共享资源不受多个线程同时访问以及修改。
代码释义
Worker类
- 这一份代码上面的基本一致,只是加了一把锁;
- 在会争夺共享资源的代码前 上锁, 在会争夺共享资源的代码后 释放锁;
Worker类
- 这一份代码上面的基本一致,只是加了一把锁;
# -*- coding: utf-8 -*-
class Worker(QThread):
valueChanged = Signal(tuple)
def __init__(self, name, mutex, main_window):
super().__init__()
self.name = name
self.mutex = mutex
self.main_window = main_window
def run(self):
for i in range(5):
# 访问共享资源的代码前 上锁
self.mutex.lock()
self.main_window.count += 1
time.sleep(0.1)
self.valueChanged.emit((self.name, self.main_window.count))
# 访问共享资源的代码后 释放锁
self.mutex.unlock()
class MainWindow(QWidget):
def __init__(self):
self.count = int()
self.mutex = QMutex() # 定义锁对象
super().__init__()
self.setup_ui()
self.setup_thread()
def setup_ui(self):
....
def setup_thread(self):
self.worker1 = Worker('thread_1', self.mutex, self)
self.worker2 = Worker('thread_2', self.mutex, self)
self.worker1.valueChanged.connect(self.thread_finished)
self.worker2.valueChanged.connect(self.thread_finished)
运行结果
现在再来看到运行结果,就是想要的结果了~
更优雅的QMutex 线程安全
查阅官网得知,在使用QMutex 锁时候,配合 QMutexLocker 使用,可以很容易地确保锁定和解锁的执行是一致的。
QMutexLocker 它的作用是在实例化时自动加锁,离开作用域时自动解锁,从而保证临界区代码的排他性,避免了资源竞争和死锁的发生。
只需要在上面 Worker类的 run中修改如下:
from PySide6.QtCore QMutexLocker
def run(self):
for i in range(5):
# 自动上锁、释放锁
with QMutexLocker(self.mutex):
self.main_window.count += 1
time.sleep(0.1)
self.valueChanged.emit((self.name, self.main_window.count))
后话
本次分享到此结束,
see you~🐱🏍🐱🏍