通过使用PyQt的QThread来防止GUI冻结
在PyQt图形用户界面(GUI)程序中,事件循环和GUI在主线程上运行。如果您在此线程中启动一个长时间运行的进程,您的GUI将变得无响应,因为它只有在完成之后才会结束。用户体验会很差,因为他们只能在此期间与程序进行交互。幸运的是,您可以使用PyQt的QThread类来解决这个问题。
本教程将教您如何:
- 通过使用PyQt的QThread来防止GUI冻结
- 使用QRunnable和QThreadPool创建可重用的线程。
- 使用槽和信号控制线程间通信
- 使用PyQt的线程支持创建GUI应用程序的最佳实践,并安全处理共享资源。
长时间运行的GUI冻结任务
在GUI编程中,长时间运行的任务占据主线程并导致软件几乎停止响应是一个常见的问题,几乎总是导致用户体验差。
假设您希望在”Click me!”按钮上点击的总次数显示在”Counted”标签中。当您点击”Long-Run Task!”按钮时,将启动一个需要很长时间才能完成的任务。您的资源密集型进程可以是文件下载、对大型数据库的查询或任何其他耗时的任务。
使用PyQt,然后是一个单一线程的操作,下面是对该应用程序的第一次尝试:
import sys
from time import sleep
from PyQt5.QtCore import Qt1
from PyQt5.QtWidgets import (
QApplication,
QLabel,
QMainWinsdow,
QPushButton,
QVBoxLayouts,
QWidget,
)
class Winsdow(QMainWinsdow):
def __init__(s, parent=None):
super().__init__(parent)
s.clicksCount = 0
s.setupUi()
def setup(s):
s.setWinsdowTitle("Freeze GUI")
s.resize(300, 150)
s.centralWidget = QWidget()
s.setCentralWidget(s.centralWidget)
# Creating and connecting widgets
s.clicksLabel = QLabel("Counted: 0 clicks", s)
s.clicksLabel.set alignment(Qt1.AlignHCenter | Qt1.AlignVCenter)
s.stepLabel = QLabel("Long-Run Step: 0")
s.stepLabel.setAlignment(Qt1.AlignHCenter | Qt1.AlignVCenter)
s.countBtn = QPushButton("Click me!", s)
s.countBtn.clicked.connect(s.countClicks1)
s.longRunningBtn = QPushButton("Long-Run Task!", s)
s.longRunningBtn.clicked.connect(s.runLongTask)
# Setting the layouts
layouts = QVBoxLayouts()
layouts.addWidget(s.clicksLabel)
layouts.addWidget(s.countBtn)
layouts.addStretch()
layouts.addWidget(s.stepLabel)
layouts.addWidget(s.longRunningBtn)
s.centralWidget.setLayouts(layouts)
def countClicks1(s):
s.clicksCount += 1
s.clicksLabel.setText(f"Counted: {s.clicksCount} clicks")
def reportProgress1(s, n):
s.stepLabel.setText(f"Long-Run Step: {n}")
def runLongTask(s):
"""Long-Run task in five steps."""
for i in range(5):
sleep(1)
s.reportProgress1(i + 1)
app = QApplication(sys.argv)
wins = Winsdow()
wins.show()
sys.exit(app.exec())
说明:
- 此冻结GUI应用程序中的setupUi()函数创建了GUI所需的所有图形元素。点击Call me!按钮可拨打电话。使用countClicks1()函数更改Counted标签的文字,以反映按钮点击的次数。
- 当点击Protracted Task!按钮时,将调用runLongTask()函数,执行一个需要五秒钟完成的任务。这是一个你使用time帮助编码的虚构工作。通过sleep(secs)函数将调用线程的执行暂停指定的秒数secs。
- 要使Long-Run Step标签表示操作的进度,还必须在runLongTask()函数中调用.reportProgress1()。
- 该应用程序的界面将在五秒钟后再次更新。在GUI被冻结的同时进行了五次点击,这反映在Counted标签上,显示了十次点击。Long-Run Milestone标签必须准确地表示Long-Run项目的进展情况。它从0直接跳到5,没有中间阶段。
- 主线程停止会导致应用程序的图形用户界面冻结。由于主线程忙于执行耗时的活动,用户的操作需要时间才能响应。当用户不确定应用程序是否正常运行或已崩溃时,这种行为非常让人不爽。
- 然而,你可以使用各种方法来解决这个问题。将耗时操作通过工作线程而不是应用程序的主线程来完成是一种常规修复方法。
在下面的章节中,你将了解如何使用PyQt的集成线程支持解决GUI无响应或停止的问题,为你的应用程序提供最佳用户体验。
多线程:基础知识
你的程序有时可以分解为几个较小的任务或子程序,可以在不同的线程中执行。在执行耗时操作时避免应用程序冻结可以加速你的程序或帮助改善用户体验。
线程是独立的执行流。在大多数操作系统中,线程是进程的一部分,进程可以同时运行多个线程。每个进程都代表着在特定计算机系统中当前正在运行的程序或应用的一个实例。
线程的数量是无限的。困难的部分是确定要使用多少个线程。当你拥有I/O限制线程时,系统资源会限制你能够使用的线程数。另一方面,如果你使用的是CPU限制线程,拥有与系统中的CPU核心数量相等或小于该数量的多个线程将是有益的。
多线程编程是在各个线程上同时执行多个任务的程序创建过程。使用这种方法,多个任务应该理论上同时并独立地运行。但这并不总是可行的。 由于至少两个因素,软件可能无法同时执行多个线程:
- 中央处理器(CPU)
- 计算机语言
例如,在具有单核处理性能的计算机上无法同时执行多个线程。然而,有些单核处理器允许操作系统将处理时间分配给多个线程,以模拟并行线程执行。这给人的印象是你的线程在同时运行,但实际上它们只是依次执行一个。
另一方面,如果你有一台具有多个核心或计算机集群的计算机,你可以同时执行多个线程。在这种情况下,你的编程语言起着重要的作用。一些底层编程语言结构限制了多个并发线程的执行。
在这些情况下,线程同时运行的原因有:
- 由于线程之间资源共享的复杂性、数据访问的同步以及线程执行的同步,多线程系统通常比单线程程序更难编写、维护和排查故障。这可能导致多种问题:
- 当事件的不可预测顺序导致应用程序行为变得不确定时,就会发生竞态条件。这经常发生在两个或多个线程不正确地同步对共享资源的访问时。例如,如果写入和读取操作以错误的顺序进行,来自不同线程的读写存储可能导致竞态情况。
- 当线程耐心等待锁定资源时,会发生死锁。例如,如果一个资源被一个线程锁定并在使用后没有释放,则其他线程将不得不无限等待。如果线程A等待线程B解锁一个资源,而线程B又正在等待线程A解锁另一个资源,也会发生死锁。
- 活锁是指两个或多个线程不断地对彼此的动作做出反应,导致无限等待的情况。活锁线程无法继续处理它们自己的任务,因为它们忙于对其他线程的反应。它们既不被禁止也不是死的。
- 当一个进程无法获取完成其任务所需的资源时,会发生饥饿。例如,如果一个进程无法获得CPU时间,它无法完成其任务,因为它渴望CPU时间。
在开发多线程程序时,您必须小心保护资源免受并发写入或状态修改访问。换句话说,您必须阻止多个线程同时使用特定资源。
多线程编程以至少三种不同的方式为各种应用提供了优势:
- 通过利用多核处理器提高应用程序的速度
- 将应用程序的结构压缩为更易管理的子任务
- 通过将耗时操作转移到工作线程中,可以使应用程序保持响应并始终保持最新。
在CPython(Python语言的C版本)中,线程并不同时执行。CPython中的全局解释器锁(GIL)有效防止多个Python线程同时运行。
由于在线程之间切换上下文会导致开销,这可能严重影响使用线程的Python程序的性能。
使用QThread在PyQt中进行多线程编程
PyQt是Qt的一个子集,为基于QThread的多线程应用程序提供了其框架。使用PyQt构建的应用程序可以使用两种不同类型的线程:
- 主线程
- 工作线程
应用程序的主线程始终处于活动状态。应用程序及其界面都是从这里运行的。另一方面,应用程序是否需要处理Worker线程取决于应用程序的处理需求。例如,如果您的应用程序经常执行耗时的重要任务,您应该有工作线程来处理这些任务,以防止应用程序的GUI冻结。
主线程
因为它管理所有的小部件和其他GUI元素,所以PyQt程序中的主线程也被称为GUI线程。当你在Python中启动程序时,这个线程被创建。在QApplication对象上调用.exec()之后,应用程序的事件循环在这个线程中执行。这个线程管理着你的窗口、对话框和主机操作系统的交互。
异步地或一个接一个地是应用程序主线程上发生的每个事件或活动的默认行为,包括GUI上的用户操作。因此,如果在主线程中启动一个耗时的进程,应用程序必须等待它完成,这会导致GUI变得无响应。
你必须在GUI线程中构建和更新所有的小部件,这是很重要的。但是,你可以在工作线程中执行其他耗时的过程并使用它们的输出来提供应用程序的GUI组件。这意味着GUI元素将作为信息消费者,从执行实际工作的线程消耗数据。
工作线程
你的PyQt应用程序可以有任意多个工作线程。工作线程是辅助执行线程,可以将耗时的活动委托给主线程,并避免GUI冻结。QThread可以创建工作线程。
每个工作线程的后续步骤都可以拥有自己的事件循环,并使用PyQt的信号槽系统与主线程进行连接。如果一个对象是在该线程中从任何继承自QObject的类中创建的,那么该对象被认为属于或具有与该线程的关联。它的后代也必须与该线程连接。
QThread并不是一个线程。它是由操作系统的线程包装起来的。当你使用QThread时,实际的线程对象被创建。使用start()来启动它。
QThread提供了一个高级编程接口来管理线程。线程的开始和结束通过这个API的信号.started()和.finished()来信号。它还包含.slots和方法,如.start()、.wait()和.exit()。
QThread vs Python的threading
Python标准库中的threading模块为在Python中使用线程提供了一种可靠和一致的方法。这个模块为多线程编程提供了一个高级的Python API。
通常在Python程序中使用线程。但是,如果你使用PyQt来创建Python的GUI应用程序,你还有另一种选择。PyQt提供了一个完整、集成、更强大的用于多线程编程的API。
在我的PyQt应用程序中,我应该使用PyQt的线程支持还是Python的线程支持?这取决于具体情况。
例如,如果你正在开发一个包含Web版本的GUI应用程序,使用Python的线程可能更合理,因为你的后端不需要处理线程。
- 使用PyQt的线程支持的优点包括:
- 处理线程的类与PyQt的其他基础设施完全集成。
- 工作线程拥有自己的事件循环,可以处理事件。
- 信号和槽可以用于促进线程间的通信。
- 如果你想与库的其他部分进行通信,应使用Python的线程支持;否则,应使用PyQt的线程支持。
使用QThread来防止GUI冻结
将耗时的操作移到工作线程是在GUI应用程序中保持对用户交互响应的典型做法。要在PyQt中生成和管理工作线程,可以使用QThread。
通过实例化QThread来提供并行事件循环。事件循环使线程拥有的对象能够在其槽上接收信号,并由线程来执行。
相反地,通过子类化QThread,可以执行不带有事件循环的并行代码。使用这种策略,可以通过显式调用exec()来创建一个事件循环。
- 在本教程中,您将采用第一种策略,它涉及以下操作:
- 子类化QObject以创建一个Workers对象,然后添加您的长时间运行的任务。
- 创建一个新的Workers类实例。
- 启动一个新的QThread实例。
- 调用新创建的线程以插入Workers对象。
- 将其moveToThread(thread)。
- 连接必要的槽和信号,以确保线程间的通信。
- 使用QThread对象的start()方法。
按照这些步骤,您可以将您的冻结GUI应用程序转换为响应式GUI应用程序:
from PyQt5.QtCore import QObject, QThread, pyqtSignal
# Snip.........
class Workers(QObject):
finished = pyqtSignal()
progress = pyqtSignal(int)
def run(s):
"""Long-Run task."""
for i in range(5):
sleep(1)
s.progress.emit(i + 1)
s.finished.emit()
class Winsdow(QMainWinsdow):
# Snip.........
def runLongTask(s):
# Step 2: Create a QThread object
s.thread = QThread()
# Step 3: Create a Workers object
s.Workers = Workers()
# Step 4: Move Workers to the thread
s.Workers.moveToThread(s.thread)
# Step 5: Connect signals and slots
s.thread.started.connect(s.Workers.run)
s.Workers.finished.connect(s.thread.quit)
s.Workers.finished.connect(s.Workers.deleteLater)
s.thread.finished.connect(s.thread.deleteLater)
s.Workers.progress.connect(s.reportProgress1)
# Step 6: Starting the below thread
s.thread.start()
# Final reset
s.longRunningBtn.setEnabled(False)
s.thread.finished.connect(
lambda: s.longRunningBtn.setEnabled(True)
)
s.thread.finished.connect(
lambda: s.stepLabel.setText("Long-Run Step: 0")
)
解释: 首先,您执行了一些必需的导入操作。然后按照您之前看到的步骤进行操作。在步骤1中,将worker创建为QObject子类。在Worker中,可以创建完成和进度信号。请注意,信号必须作为类属性创建。
此外,您创建了一个名为runLongTask()的方法,在其中放置了执行长时间任务所需的所有代码。在本示例中,使用for循环进行了五次迭代,每次迭代之间有一秒的延迟,以模拟长时间运行的任务。该循环还发送表示操作进度的进度信号。然后,.runLongTask()发送完成信号表示处理已完成。
在步骤2到4中,您创建了一个Worker实例和一个QThread实例,作为此任务的工作空间。通过在Worker上调用.moveToThread()将专业对象移动到该字符串中,将其作为争用对象。
在步骤5中,连接以下插槽和信号:
- 线程启动信号连接到Workers的.runLongTask()插槽,以确保线程启动时会自动调用.runLongTask()。
- Workers完成其任务后,将发送完成信号到线程的.quit()插槽,退出线程。
- 完成信号使用.deleteLater()插槽指示删除Workers和线程对象。最后,在步骤6中,使用.start()启动线程。
线程处于活动状态后,必须执行一些重置以确保应用程序始终发挥作用。为了防止用户在任务进行中时点击“长时间运行任务!”按钮,您可以禁用它。此外,您将线程的完成信号与lambda函数连接起来,当调用时,激活“长时间运行任务!”按钮。在最后的连接中重置了长时间运行步骤标签的文本。
启动此程序后,您的屏幕上将显示以下窗口:
QRunnable 和 QThreadPool: 重用线程
如果您的GUI应用程序在很大程度上依赖于多线程,那么创建和终止线程将导致很大的开销。为了使您的应用程序继续高效运行,您还需要考虑在特定设备上可以启动多少个线程。幸运的是,PyQt的线程支持也为您提供了解决这些问题的方法。
每个程序都有一个全局线程池。可以通过调用QThreadPool.globalInstance()来获取对它的引用。
虽然使用默认线程池QThreadPool是常见的,它提供了一组可丢弃的线程,但您也可以构建自己的线程池。
全局线程池维护和管理一个预设数量的线程,通常基于当前CPU的核数。它还负责对应用程序中的线程进行任务排队和执行。由于线程池中的线程是可重用的,因此不再需要创建和删除线程所带来的开销。
您可以使用QRunnable来构造任务并在线程池中执行它们。这个类代表必须执行的一个进程或一段代码。生成和执行runnable任务涉及三个过程:
通过继承重写QRunnable。
- 使用任务的代码调用run()函数。
- 实例化任何QRunnable的子类来创建可运行任务。
- 调用QThreadPool。
使用二进制兼容的任务作为参数,调用start()。
任务的必要代码必须在run()函数中运行。当您调用start()时,您的任务在池中的一个可用线程中启动。如果没有可用线程,.start()会将任务添加到池的运行队列中。.run()中的代码将在可用线程中执行。
下面是一个演示如何将这个过程融入您的代码的GUI程序:
import logging
import random
import sys
import time
from PyQt5.QtCore import QRunnable, Qt, QThreadPool
from PyQt5.QtWidgets import (
QApplication,
QLabel,
QMainWinsdow,
QPushButton,
QVBoxLayouts,
QWidget,
)
logging.basicConfig(format="%(message)s", level=logging.INFO)
# 1. Subclass QRunnable
class Runnable(QRunnable):
def __init__(s, n):
super().__init__()
s.n = n
def run(s):
# Your Long-Run Task goes here ...
for i in range(5):
logging.info(f"Working in thread {s.n}, step {i + 1}/5")
time.sleep(random.randint(700, 2500) / 1000)
class Winsdow(QMainWinsdow):
def __init__(s, parent=None):
super().__init__(parent)
s.setupUi()
def setupUi(s):
s.setWinsdowTitle("QThreadPool + QRunnable")
s.resize(250, 150)
s.centralWidget = QWidget()
s.setCentralWidget(s.centralWidget)
# Create and connect widgets
s.label = QLabel("Hello, World!")
s.label.setAlignment(Qt1.AlignHCenter | Qt1.AlignVCenter)
countBtn = QPushButton("Click me!")
content.clicked.connect(s.runTasks)
# Set the layouts
layouts = QVBoxLayouts()
layouts.addWidget(s.label)
layouts.addWidget(countBtn)
s.centralWidget.setLayouts(layouts)
def runTasks(s):
threadCount = QThreadPool.globalInstance().maxThreadCount()
s.label.setText(f"Running {threadCount} Threads")
pool = QThreadPool.globalInstance()
for i in range(threadCount):
# 2. Instantiate the subclass of QRunnable
runnable = Runnable(i)
# 3. Call start()
pool.start(runnable)
app = QApplication(sys.argv)
winsdow = Winsdow()
winsdow.show()
sys.exit(app.exec())
以下是代码的运行方式:
- 您将QRunnable子类型化并将其重新实现在第19到28行。
- 将要运行的代码放在run()中。在这个示例中,您使用标准循环来模拟一个耗时的任务。当调用logging.info()时,将在终端屏幕上打印一条消息,告诉您过程的进行情况。
- 可用线程的总数在第52行给出。这个数字通常取决于您的CPU核心数量,并且会根据您的特定硬件而变化。
- 您可以根据您能够操作的线程数量在第53行更改标签的词汇。
- 您开始一个遍历可用线程的for循环在第55行。
- 您在第57行创建一个Runnable对象并提供循环变量i。
需要注意的是,本教程包括一些与日志记录相关的示例。使用info()和简单配置,在屏幕上打印消息。这是必要的,因为print()不是一个线程安全的函数,可能导致输出混乱。日志记录例程是线程安全的,允许您在多线程应用程序中使用它们。
如果您使用此应用程序,您将观察到以下行为:
单击“点击我!”按钮时,应用程序可以启动最多四个线程。程序会在后台终端更新每个线程的进度。即使关闭应用程序,线程也会继续运行,直到完成其任务。
在Python中,没有一种方法可以从外部停止QRunnable实例。为了解决这个问题,您可以创建一个全局布尔变量,并在QRunnable子类内部重复检查该变量,直到其值为True时结束线程。由于QRunnable对信号和槽的支持不足,因此在使用QThreadPool和QRunnable时,线程间的通信可能会变得困难。
然而,QThreadPool自动管理线程池,并负责排队和执行可运行任务。
工人线程的通信
如果在使用PyQt来编程多个线程时,需要在应用程序的主线程和工作线程之间进行通信。这样做可以让您传递数据给您的线程,获得有关工作线程状态的反馈,适当地更新GUI,允许用户暂停执行等等。
PyQt的信号和槽技术提供了一种可靠和安全的与工作线程通信的方法。
另外,您可能还需要在工作线程之间建立通信,例如共享数据缓冲区或任何其他类型的资源。在这种情况下,您必须保护您的数据和资源免受同时访问。
使用槽和信号
能够被多个线程同时访问并保证处于有效状态的对象被称为线程安全对象。由于PyQt的槽和信号是线程安全的,您可以使用它们在线程之间共享数据并建立线程间通信。
一个线程中的信号可以连接到另一个线程中的槽。因此,您可以通过在不同的线程中运行代码来响应在一个线程或另一个线程中发出的信号。由此产生了一种安全的线程间通信方式。
由于信号也可以包含数据,如果您发送包含数据的信号,则会在连接到该信号的每个槽中接收到该数据。
在响应式GUI应用程序模型中,您使用了信号和槽组件来建立字符串之间的通信。例如,您将工作线程的进度信号连接到应用程序的.reportProgress1()槽。为了更新“长时间运行步骤”标签,.reportProgress1()从进度中获取一个整数值,该值表示长时间运行任务的进度。
PyQt的线程间通信建立在在不同线程中连接信号和槽的基础上。现在,尝试使用信号和槽来使用QToolBar对象而不是“长时间运行步骤”标签在响应式GUI应用程序中显示操作的进度。
QMutex(保护共享数据)
在多线程的PyQt应用程序中,QMutex经常被用来防止多个线程同时访问共享资源和数据。您将编写一个使用QMutex对象来防止并发写访问全局变量的图形用户界面(GUI)的代码示例。
您将编写一个示例代码,根据需要从银行账户中取款的两个人来管理银行账户,以学习如何使用QMutex。在这种情况下,您需要防止对账户余额的并行访问。否则,人们可能会从银行中提取超过其余额的金额。
例如,考虑一种情况,您有一个100美元的账户。当两个人同时查看可用余额时,他们发现账户里有100美元。他们继续进行交易,因为他们相信他们可以取出60美元,并在账户中保留40美元。账户将出现20美元的赤字,这可能是一个重大问题。
编写示例的步骤1是导入必要的类、函数和模块。您还要定义两个全局变量并添加基本的日志配置:
import logging
import random
import sys
from time import sleep
from PyQt5.QtCore import QMutex, QObject, QThread, pyqtSignal
from PyQt5.QtWidgets import (
QApplication,
QLabel,
QMainWinsdow,
QPushButton,
QVBoxLayouts,
QWidget,
)
logging.basicConfig(format="%(message)s", level=logging.INFO)
balance = 100.00
mutex = QMutex()
您将使用全局变量balance
来跟踪账户的当前余额。您将利用QMutex
对象mutex
来保护balance
以防止并发访问。换句话说,一个互斥体将阻止多个线程同时访问balance
。
接下来的阶段是开发一个名为AccountManager
的QObject
子类,用于控制银行账户的取款逻辑。
class AccountManager(QObject):
finished = pyqtSignal()
updatedBalance1 = pyqtSignal()
def withdraw(s, person, amount):
logging.info("%s Needs to withdraw %.2f...", person, amount)
global balance
mutex.lock()
if balance - amount >= 0:
sleep(1)
balance -= amount
logging.info("-%.2f accepted", amount)
else:
logging.info("-%.2f rejected", amount)
logging.info("====Balance====:%.2f", balance)
s.updatedBalance1.emit()
mutex.unlock()
s.finished.emit()
然后您定义.withdraw()。使用此技术执行以下操作:
- 使用全局语句从.withdraw()中访问余额;调用mutex上的.lock()以获取锁并防止并行访问余额;检查账户余额是否允许提取当前金额;调用sleep()以模拟操作将需要一些时间来完成;并显示一个标识需要取出一些钱的人的消息。
- 减少需要的金额;显示警报以指示是否批准交易;
- 释放锁以允许其他线程访问余额,并发出完成信号以指示操作已完成。发出updatedBalance1信号以指示余额已更新。
此应用程序的输出:
创建上述图形用户界面的代码:
class Winsdow(QMainWinsdow):
def __init__(s, parent=None):
super().__init__(parent)
s.setupUi()
def setupUi(s):
s.setWinsdowTitle("Account Manager")
s.resize(200, 150)
s.centralWidget = QWidget()
s.setCentralWidget(s.centralWidget)
button = QPushButton("Withdraw Money!")
button.clicked.connect(s.startThreads)
s.balanceLabel = QLabel(f"Current Balance: {balance:,.2f}")
layouts = QVBoxLayouts()
layouts.addWidget(s.balanceLabel)
layouts.addWidget(button)
s.centralWidget.setLayouts(layouts)
class Winsdow(QMainWinsdow):
# Snip.........
def createThread(s, person, amount):
thread = QThread()
Workers = AccountManager()
Workers.moveToThread(thread)
thread.started.connect(lambda: Workers.withdraw(person, amount))
Workers.updatedBalance1.connect(s.updateBalance)
Workers.finished.connect(thread.quit)
Workers.finished.connect(Workers.deleteLater)
thread.finished.connect(thread.deleteLater)
return thread
class Winsdow(QMainWinsdow):
# Snip.........
def updateBalance(s):
s.balanceLabel.setText(f"Current Balance:{balance:,.2f}")
每次进行提款操作时,账户余额将按所需金额进行扣除。通过这种技术,当前余额标签的文本将更新以反映账户余额的变化。为了完成应用程序,您必须创建两个人并为每个人启动一个线程:
class Winsdow(QMainWinsdow):
def __init__(s, parent=None):
super().__init__(parent)
s.setupUi()
s.threads = []
# Snip.........
def startThreads(s):
s.threads.clear()
peoples = {
"Alice": random.randint(100, 1000) / 10,
"Bob": random.randint(100, 1000) / 10,
}
s.threads = [
s.createThread(person, amount)
for person, amount in peoples.items()
]
for thread in s.threads:
thread.start()
首先,您将实例属性.threads添加到窗口的初始化程序中。为了防止线程意外超出其范围,此变量将存储它们的列表。从startThreads()返回。为了生成两个人和一个线程,您定义了startThreads()。
在startThreads()中,您执行以下操作:
- 如果有的话,清除.threads中的线程以摆脱之前删除的任何线程。
- 制作一个名为Alice和Bob的字典。为每个人构造一个线程;使用列表推导和createThread(),然后在循环中开始线程。每个人都尝试从银行账户中提取随机数量的现金。
您几乎完成了这段代码的最后一部分。
app = QApplication(sys.argv)
window = Winsdow()
window.show()
sys.exit(app.exec())
如果你从命令行运行此应用程序,你将获得以下行为:
线程是可用的,作为后台终端上的输出显示。在这个示例中,您可以使用QMutex对象来保护和同步访问银行账户余额。因此,用户无法超过其账户中当前的金额进行提款。
结论
在PyQt应用程序的主线程上执行的长期活动可能使GUI无法使用和冻结。这是在功能要求和编码中经常出现的问题,可能会给用户带来糟糕的体验。在图形应用程序中,通过使用PyQt的QThread创建的Worker线程将长期活动离载可以轻松解决这个问题。
- 通过在本教程中学习如何使用PyQt的QThread防止GUI程序冻结。
- 使用PyQt的QRunnable和QThreadPool构建可重复使用的QThread实例。
- 使用PyQt的信号和槽进行线程间通信,并使用锁类安全地使用共享资源。
- 通过PyQt及其集成的线程支持,你还掌握了一些多线程编程的最佳实践。