PySide6/PyQT多线程之 线程安全:互斥锁条件变量的最佳实践

news2024/10/5 21:23:51

前言

PySide6/PyQT中使用多线程时,线程锁和线程安全是非常重要的概念。本文将介绍线程锁和线程安全的基本概念,以及如何在PySide6/PyQT中使用它们。

使用PySide6/PyQT开发GUI应用程序,在多个线程同时访问同一个共享对象时候,如果没有进行同步处理那就可能会导致数据不一致或者一些意料之外的问题发生。因此,确保线程安全是非常重要的。

而说到线程安全,最简单的处理方法就是用 互斥锁条件变量。

所以本文着力介绍PySide6/PyQT 的三个组件:QMutexQMutexLockerQWaitCondition ,以解决 线程安全 的问题。



先说结论:

  • 只使用QMutex就可以实现线程安全,但是QWaitCondition能够更加精细地控制线程的运行。

代码在下面,直接拿来就能用。
这篇文章写的我很累,以后不考虑分享这么乱糟糟的文章了。


知识点📖📖

本文用到的几个PySide6的知识点及链接。

作用链接
创建新线程QThread
对象间通信的机制,允许对象发送和接收信号Signal
用于响应Signal信号的方法Slot
线程同步机制,用于协调多个线程之间对共享资源的访问QMutex
锁定互斥锁的对象,简化代码,避免手动处理锁的加锁和解锁操作QMutexLocker
线程同步机制,一般配合 QMutex 使用QWaitCondition

多线程通信和同步

在多线程的编程中,线程之间的通信和同步是绕不开的话题。

通信:

  • PySide6/PyQT 提供了信号槽(Signal and Slot) 机制,它们用于在线程之间传递消息和触发事件。通过在不同的线程中发送信号和连接槽函数,可以实现线程间的通信。

同步:

  • 在共享对象被多个线程同时访问时候容易出现意料之外的问题,需要保护好资源争夺;
  • 互斥锁(QMutex)条件变量(QWaitCondition) 等同步机制可以用于控制线程的并发访问,确保线程安全和避免线程竞争。

注意事项

关于保证线程安全,可以遵循以下几个原则:

  • 尽量不要在多个线程中访问和修改同一个对象;
  • 如果必须要访问和修改同一个对象,需要使用线程同步机制,例如信号槽、互斥锁、条件变量等;
  • 避免使用共享状态,例如全局变量,尽量将状态封装在对象内部,并使用线程安全的方式访问和修改状态;
  • 不要使用原生的线程库,选择使用 Qt 提供的 QThreadQThreadPool 等线程库。

上面这几点其实差不多一个意思,有些概念就行。

互斥锁

下面这份代码使用了 QMetuxQMutexLocker

# -*- coding: utf-8 -*-
# Name:         demo3.py
# Author:       小菜
# Date:         2023/5/4 
# Description:

import sys

from PySide6.QtCore import (QThread, Signal, Slot, QMutex)
from PySide6.QtWidgets import (QApplication, QLabel, QPushButton, QVBoxLayout, QWidget)


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):
            with QMutexLocker(self.mutex):
                self.main_window.count += 1
                self.msleep(100)
                self.valueChanged.emit((self.name, self.main_window.count))


class MainWindow(QWidget):
    def __init__(self):
        self.count = int()
        self.mutex = QMutex()  # 定义锁对象
        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.mutex, self)
        self.worker2 = Worker('thread_2', self.mutex, 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()
    ex = MainWindow()
    sys.exit(app.exec())

代码释义

Worker类

  • 继承了QThread类,并创建 信号valueChanged
  • 接收两个参数,一个为name,一个为MainWindow类实例对象本身;
  • 循环5次,每次为 MainWindow实例count累加1;
  • 并使用valueChanged 信号将执行结果和执行次数发送出去;

MainWindow类

  • 继承了QWidget,实现了包含一个按钮和一个标签的窗口;
  • setup_ui函数为窗口布局;
  • setup_thread函数 实例化两个Worker类,并将它们的信号连接到Slot槽函数 thread_finished
  • 按钮绑定了 start_threads函数
  • thread_finished函数Slot槽函数,用于接收 Worker 的信号发送的结果。

代码中两个线程同时访问了 self.count,结合本篇文章标题,看看下面的运行结果。


运行结果

代码运行效果如下图所示:

  • 左边是没有考虑线程安全的,右边是上面代码运行结果;
  • 如果程序没有出错,那应该是按照顺序打印 1~10
  • 左边,这个并不是按照顺序的,说明它们在互相争夺共享资源 self.count时候出现了岔子,这是不推荐的;
  • 右边,是线程安全的,是推荐的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2i0gKjZC-1683282690632)(image/PySide6PyQT多线程之 线程安全:线程锁&条件变量的最佳实践/image-20230505174641931.png)]

QMetux & QMetuxLocker

PySide6中可以通过QMutex实现线程锁。

QMutex是一个用于同步线程执行的互斥锁,可以保护共享资源不受多个线程同时访问以及修改;

QMutexLocker可以简化 QMutex 的代码,避免手动处理锁的加锁和解锁操作 ,且避免了资源竞争和死锁的发生。

在线程编程中,竞态条件是一种常见的问题。当多个线程尝试同时修改共享资源时,可能会发生竞态条件,导致程序出现意外的行为。为了解决这个问题,可以使用线程锁来保护共享资源。

下面是一个简单的示例,展示如何使用 QMutex + QMutexLocker 来保证线程安全。

from PySide6.QtCore import QObject, QMutex, QMutexLocker

class MyObject(QObject):
    def __init__(self):
        super().__init__()
        self.shared_data = list()

        # 创建互斥锁
        self.mutex = QMutex()

    def add_data(self, data):
        # 不需要手动解锁,QMutexLocker会在离开作用域时自动解锁
        with QMutexLocker(self.mutex):
            self.shared_data.append(data)

上面代码包含了一个共享的列表shared_data

add_data 方法中,使用 QMutex + QMutexLocker 方法锁定了共享资源,然后向列表中添加了一个新的元素。

这里使用了QMutexLocker,它也会在离开add_data方法时自动释放锁。这样就确保了线程安全,即使出现异常也不会影响其他线程的访问。


QWaitCondition

QWaitCondition 是一种同步机制,可以用来协调线程之间的操作。它通过让线程进入等待状态,等待某个条件成立来保证线程安全

在线程安全中,使用QWaitCondition 不是必须的。只是用了 能够更加精细地控制线程的运行。

一些概念

QWaitCondition 是 PySide6 中的一个同步机制,它可以阻塞一个线程,直到收到一个信号通知。

QWaitCondition 主要由三个方法构成:

  • wait(mutex: QMutex, time: int = ULONG_MAX): 阻塞当前线程,直到收到该 QWaitCondition 对象的信号,或等待时间超时。在等待期间,会释放 mutex
  • wakeOne(): 发送一个信号来唤醒一个等待在该 QWaitCondition 上的线程。如果没有线程等待,则该方法没有任何效果;
  • wakeAll(): 发送一个信号来唤醒所有等待在该 QWaitCondition 上的线程。

使用 QWaitCondition 的基本步骤是:

  1. 创建一个 QWaitCondition 对象和一个 QMutex 对象,并将它们传递给需要协调的线程。

  2. 在需要等待信号的线程中,使用 wait() 方法阻塞线程。

  3. 在发送信号的线程中,使用 wakeOne()wakeAll() 方法发送信号。




代码

is_paused 标志为 True 时,线程会调用 self.cond.wait(self.mutex) 阻塞自己,等待 resume_thread() 方法的调用。
resume_thread() 方法被调用后,会将 is_paused 设置为 False,然后调用 self.cond.wakeOne() 发送信号来唤醒被阻塞的线程。

在线程中使用了条件变量 self.cond 和互斥锁 self.mutex 来控制线程的暂停和恢复,QWaitCondition 可以让线程在等待状态时休眠,直到某个条件被满足并且可以被唤醒。这样避免线程在忙等待时占用 CPU 资源,并减少程序的资源消耗。

# -*- coding: utf-8 -*-
# Name:         demo.py
# Author:       小菜
# Date:         2023/5/4
# Description:

import sys
from PySide6.QtCore import (QThread, QWaitCondition, QMutex, Signal, QMutexLocker)
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QPushButton, QProgressBar, QApplication)


class MyThread(QThread):
    valueChange = Signal(int)

    def __init__(self):
        super().__init__()
        self.is_paused = bool()
        self.progress_value = int(0)
        self.mutex = QMutex()
        self.cond = QWaitCondition()

    def pause_thread(self):
        with QMutexLocker(self.mutex):
            self.is_paused = True

    def resume_thread(self):
        if not self.is_paused:
            return
        with QMutexLocker(self.mutex):
            self.is_paused = False
            # 释放其它线程
            self.cond.wakeOne()

    def run(self):
        while True:
            with QMutexLocker(self.mutex):
                while self.is_paused:
                    # 阻塞当前线程
                    self.cond.wait(self.mutex)
                if self.progress_value > 100:
                    self.progress_value = 0
                    return
                self.progress_value += 1
                self.valueChange.emit(self.progress_value)
                self.msleep(10)


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setup_ui()
        self.setup_thread()

    def setup_ui(self):
        layout = QVBoxLayout(self)
        self.progressBar = QProgressBar(self)
        layout.addWidget(self.progressBar)
        layout.addWidget(QPushButton(r'启动&&停止', self, clicked=self.paused_thread))
        layout.addWidget(QPushButton('恢复线程', self, clicked=self.wake_thread))
        self.show()

    def setup_thread(self):
        self.thread = MyThread()
        self.thread.valueChange.connect(self.progressBar.setValue)

    def paused_thread(self):
        if not self.thread.isRunning():
            self.thread.start()
        else:
            self.thread.pause_thread()

    def wake_thread(self):
        self.thread.resume_thread()


if __name__ == '__main__':
    app = QApplication()
    window = MainWindow()
    sys.exit(app.exec())

代码释义

MyThread类

  • MyThread 继承自 QThread,重写run方法
  • while 循环中更新进度条的值,并通过信号 valueChange 发送更新后的进度值
  • 包含一个互斥量 mutex 和一个等待条件 cond,用于实现线程的暂停和恢复

MainWindow类

  • setup_ui() 中创建了一个进度条和两个按钮,分别用于启动/停止线程和恢复线程;
  • setup_thread() 中创建了一个 MyThread 对象,并连接了其信号 valueChange 和界面上的进度条;
  • paused_thread() 方法用于启动/停止线程,如果线程没有启动,则启动线程;
  • 如果线程已经启动,则调用 MyThread 中的 pause_thread() 方法,将线程暂停;
  • wake_thread() 方法用于恢复线程,调用 MyThread 中的 resume_thread() 方法,将线程从暂停中恢复。

运行结果

总结✨✨

只使用QMutex就可以实现线程安全,但是加上QWaitCondition能够更加精细地控制线程的运行。

后话

本次分享到此结束,
see you~🐱‍🏍🐱‍🏍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/494871.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

单词词义、词性、例句查询python代码

单词发音、词义、词性、例句查询、输出结果更简洁,一次可查多个单词 运行该代码,命令窗口输入单词,单词用“/”分开,例如:noisy/problem/community/neighbor 可以更多。先安装两个python包requests、 beautifulsoup4&…

Eureka详解

Eureka概述和架构 Eureka Spring Cloud Eureka 是Netflix 开发的注册发现组件,本身是一个基于 REST 的服务。提供注册与发现,同时还提供了负载均衡、故障转移等能力 Eureka3个角色 服务中心,服务提供者,服务消费者 Eureka Se…

Win11的两个实用技巧系列之蓝屏死循环解决办法、调高进程的优先级方法

Win11蓝屏死循环怎么办?Win11蓝屏死循环解决办法 有用户安装Win11系统的时候,重新启动电脑的时候,会一直进入蓝屏的错误循环中,本文就为大家带来详细的解决方法,需要的朋友一起看看吧 Win11蓝屏死循环解决办法分享。有用户安装W…

Ubuntu18.04系统及相关软件安装恢复过程

Ubuntu18.04系统及相关软件安装恢复过程 一、常用软件安装1. [系统安装](https://blog.csdn.net/qq_43309940/article/details/116656810)2. [显卡驱动安装](https://blog.csdn.net/qq_43309940/article/details/126898929)3. [ROS Melodic安装](https://ismango.blog.csdn.net…

[Python]爬虫基础——urllib库

urllib目录 一、简介二、发送请求1、urlopen()函数2、Request()函数 三、异常处理四、解析URL五、分析Robots协议 一、简介 urllib库是Python内置的标准库。包含以下四个模块: 1、request:模拟发送HTTP请求; 2、error:处理HTTP请…

实验六 UML建模工具应用

一、实验目的 1.掌握面向对象开发思想及实现机制 2.理解并掌握UML常用图(重点:类、对象图、用例图) 3.掌握并常见UML建模工具,并绘制UML各种图 二、实验准备与要求 1.StarUML(简称SU),是一种创建UML类图&#xff0c…

洛谷P8597 [蓝桥杯 2013 省 B] 翻硬币C语言/C++

[蓝桥杯 2013 省 B] 翻硬币 题目背景 小明正在玩一个“翻硬币”的游戏。 题目描述 桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母,不是零),比如可能情形是 **oo***oooo,如果…

ideaSSM医院挂号管理系统VS开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 SSM医院挂号管理系统是一套完善的完整医院类型系统,结合SSM框架和bootstrap完成本系统SpringMVC spring mybatis ,对理解JSP java编程开发语言有帮助系统采用SSM框架(MVC模式 开发),系统具有完整的源代…

leetcode 1143. 最长公共子序列

1. dp 数组的定义 下标: 以 i - 1 和 j - 1 为结尾的子序列 值:以 i - 1 和 j - 1 为结尾的最长公共子序列的长度 2. 递推公式 if(text1[i - 1] text2[j - 1]) // 相等 dp[i][j] dp[i - 1][j - 1] 1 ; elsedp[i][j] max(dp[i - 1][j],…

【问题记录】flask开发blog

文章目录 小知识点问题1. 文章标签显示错误2. 文章状态无法回显(open)3. 用户管理页面,图标无法显示4. BuildError5. 用户管理添加用户,使用重复的用户名会报错(open)6. 添加用户,不上传头像会报错(open)7. 部分标签删除时报错&am…

设计模式 Template Method Pattern(Inheritance) vs Strategy Pattern(Delegation)

Template Method Pattern 和 Strategy Pattern 是两种常用的行为设计模式。他们分别用了继承inheritance和委托delegation两种不同的实现方法,因为上篇文章讲过了UML图,所以这篇顺便可以把两种不同模式的UML图都带出来一起说明。 Template Method Patte…

Mybatis的PageHepler用法

分页原理 分页在使用时的分类 物理分页: 在操作数据库中的表时,sql语句中使用了limit ?,?,此时sql语句返回的结果是分页结果 逻辑分页: 依赖程序的代码,其原理为:通过sql语句将数据库表中的所有数据都查询出,之后将数据保存在内存中,最终要显示的数据若涉及到分页,到内存中…

Java企业级信息系统开发01—采用spring配置文件管理bean

文章目录 一、Web开发技术二、spring框架(一)spring官网(二)spring框架优点(三)Spring框架核心概念1、IoC(Inversion of Control)和容器2、AOP(Aspect-Oriented Programm…

Golang 包使用注意事项

1)在给一个文件打包时,该包对应一个文件夹,比如这里的utils文件夹对应的包名就是utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。 2)当一个文件要使用其它包函数或变量时,需要…

【AI聊天 | GPT4教学】 —— 微软 New Bing GPT4 申请与使用保姆级教程(免魔法)

目录 认识 New Bing 1. 下载 Microsoft Edge 浏览器 2. 注册并登录 Microsoft 账号 3. 如何免科学上网使用 New Bing? 4. 加入 WaitList 候补名单 5. 使用 New Bing! 6. 使用 Skype 免科学上网访问 New Bing! 7. 在 Chrome 浏览器中使…

gpt人工智能详细介绍

chatgpt人工智能怎么下载 OpenAI ChatGPT不是一款普通的软件,它是由OpenAI开发的一款基于人工智能技术的自然语言生成器。因此,它并不需要像普通软件一样下载和安装在您的计算机上。 作为一个云端服务,OpenAI ChatGPT可以通过您的浏览器直接…

HBASE入门 基本shell命令(一)

一、登录连接shell $HBASE_HOME/bin/hbase shell二、基本命令 2.1help命令 help创建命名空间 create_namespace bigdata;查看命名空间 list_namespace命名空间default和habase是系统自带的 三、DDL 3.1创建表 create bigdata:student, {NAME > name, VERSIONS> 5}…

每日学术速递5.6

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.AG3D: Learning to Generate 3D Avatars from 2D Image Collections 标题:AG3D:学习从 2D 图像集合生成 3D 头像 作者:Zijian Dong, Xu Chen, …

Amper Music:AI创意音乐工具

【产品介绍】 Amper Music 是一家位于美国纽约的人工智能音乐技术公司,成立于2014年。 Amper Music是一个AI创意音乐工具,能让任何人为自己的内容制作原创音乐。无论你需要为视频、播客或互动内容配乐,Amper Music都能提供一个简单而强大的解…

【PHP在线定制商城网站源码V3.0】开源的DIY在线定制商城系统+在线礼品定制

源码下载:https://download.csdn.net/download/m0_66047725/87637177 PHP在线定制商城网站源码,免费开源、免费下载。本商城基于mycncart开发。安装成功后即可浏览,你可以在后台->安装扩展功能上传安装插件,在代码调整中点击刷…