使用 PyQt 的 QThread 防止 GUI 冻结2025 年 1 月 11 日 | 20 分钟阅读 在 PyQt 图形用户界面 (GUI) 程序中,事件循环和 GUI 在主执行线程上运行。如果在此线程中启动一个长时间运行的进程,您的 GUI 将变得无响应,因为它只会等到该进程完成。用户体验会很差,因为他们只能在该期间与程序交互。幸运的是,您可以使用 PyQt 的 QThread 类来解决这个问题。 本教程将教您如何
长时间运行导致 GUI 冻结的任务GUI 编程中普遍存在的问题是,长时间运行的任务会占用主线程并导致软件冻结,这几乎总是导致糟糕的用户体验。 假设您希望“点击我!”按钮的点击总次数显示在“计数”标签中。当您单击“长时间运行任务!”按钮时,一个需要很长时间才能完成的任务将开始。您的资源密集型进程可能是文件下载、对大型数据库的查询或任何其他耗时任务。 以下是使用 PyQt 和单个操作线程对该应用程序进行编程的首次尝试 说明
您将在以下部分中了解如何使用 PyQt 的集成线程支持来解决无响应或停止的 GUI 问题,并在您的应用程序中提供最佳用户体验。 多线程基础知识您的程序有时可以分解为几个较小的作业或子程序,您可以在不同的线程中执行它们。在执行耗时操作时避免应用程序冻结可以加快您的程序或帮助您改善用户体验。 线程是独特的执行流。在大多数操作系统中,线程是进程的一部分,进程可以同时运行多个线程。程序或应用程序的实例表示在特定计算机系统中当前运行的每个进程。 线程的数量是无限的。困难的部分是弄清楚要使用多少线程。当您使用可用的 I/O 密集型线程时,系统资源限制了您可以使用的线程数量。另一方面,如果您正在使用 CPU 密集型线程,那么拥有的线程数量等于或少于系统中的 CPU 核心数量将是有利的。 多线程编程是创建可以在不同线程上同时执行多个任务的程序的过程。使用此方法,多个作业理想情况下应该并发且独立地运行。然而,这并非总是可行的。软件可能无法同时运行多个线程,原因至少有以下两个:
例如,您无法在具有单核处理能力的计算机上同时执行多个线程。另一方面,某些单核处理器允许操作系统将处理时间分配给多个线程,以模拟并行线程执行。这给人的印象是您的线程同时运行,而它们只是一次运行一个。 另一方面,如果您有多个核心的计算机或计算机集群,则可以同时执行多个线程。您的编程语言在这种情况下起着重要作用。一些底层编程语言结构限制了多个并发线程的执行。 在这些情况下,线程并发运行的原因很少,例如
在开发多线程程序时,您必须注意保护您的资源免受并发写入或状态修改访问。换句话说,您必须阻止多个线程同时使用某个特定资源。 多线程编程至少通过三种不同的方式为各种应用程序提供优势
在 CPython(Python 语言的 C 版本)中,线程不是同时执行的。CPython 中的全局解释器锁 (GIL) 有效地阻止了多个 Python 线程同时运行。 由于线程之间上下文切换造成的开销,这可能会严重影响使用线程的 Python 程序的性能。 PyQt 中的多线程与 QThreadPyQt 是 Qt 的一个子集,它提供了自己的框架来构建基于 QThread 的多线程应用程序。使用 PyQt 构建的应用程序可以使用两种不同类型的线程
应用程序的主线程始终处于活动状态。应用程序及其界面从此处运行。另一方面,工作线程是否存在取决于应用程序处理的需要。例如,如果您的应用程序经常执行耗时的繁重活动,则应该有工作线程来处理此类任务并防止应用程序的 GUI 冻结。 主线程由于它管理所有小部件和其他 GUI 元素,PyQt 程序中的主执行线程也称为 GUI 线程。当您在 Python 中启动程序时,会创建此线程。在对 QApplication 对象调用 .exec() 之后,应用程序的事件循环在此线程中执行。此线程管理您的窗口、对话框和主机操作系统交互。 在应用程序主线程上发生的每个事件或活动(包括用户在 GUI 上的操作)的默认行为是异步的,或者一个接一个地执行。因此,如果您在主线程中启动一个长时间运行的进程,应用程序必须等待它完成,这会导致 GUI 无响应。 您必须在 GUI 线程中构建和更新所有小部件,这一点至关重要。但是,您可以在工作线程中执行其他耗时进程,并使用它们的输出为应用程序的 GUI 组件提供信息。这意味着 GUI 元素将充当信息消费者,消耗来自执行实际工作的线程的数据。 工作线程您的 PyQt 应用程序可以拥有任意数量的工作线程。工作线程是辅助执行线程,可以将耗时活动从主线程委派出去,并避免 GUI 冻结。QThread 允许创建工作线程。 通过 PyQt 的信号和槽系统与主线程连接的能力,每个工作线程都可以拥有自己的事件循环。如果一个对象是从 QObject 派生的任何类在该线程中创建的,则认为它属于或对该线程具有亲和性。它的后代也必须连接到该线程。 QThread 不是一个线程。它封装了操作系统中的一个线程。当您使用 QThread 时,实际的线程对象是通过 .start() 生成的。 QThread 为应用程序提供高级编程接口 (API) 来管理线程。通过包含信号 .started() 和 .finished(),此 API 会发出线程启动和结束的信号。它还包含 .start()、.wait() 和 .exit() 等槽和方法。 QThread vs Python 的 threading 模块Python 标准库的 threading 模块提供了一种可靠且一致的方法来在 Python 中使用线程。此模块为多线程编程提供了高级 Python API。 通常,threading 在 Python 程序中使用。但是,如果您正在使用 PyQt 创建带有 Python 的 GUI 应用程序,您还有另一种选择。PyQt1 提供了一个完整、集成、更强大的多线程 API。 我应该在 PyQt 应用程序中使用 PyQt 的线程支持还是 Python 的线程支持?答案是视情况而定。 例如,如果您正在开发一个包含 Web 版本的 GUI 应用程序,那么 Python 的线程更有意义,因为您的后端不需要处理线程。 使用 PyQt 线程支持的优点包括:
使用 QThread 防止 GUI 冻结在 GUI 应用程序中将耗时活动卸载到工作线程是一种常见的做法,可以使 GUI 对用户交互保持响应。在 PyQt 中,使用 QThread 生成和管理工作线程。 通过实例化 QThread,可以提供一个并行事件循环。事件循环使线程拥有的对象能够在其槽上接收信号,并由线程执行它们。 相反,子类化 QThread 可以在没有事件循环的情况下执行并行代码。使用此策略,您可以通过显式调用 exec() 来创建事件循环。 本教程将采用第一种策略,它需要执行以下操作
通过遵循这些步骤,您可以将冻结的 GUI 应用程序转换为响应式 GUI 应用程序 解释: 首先,您导入一些必需的模块。然后按照前面看到的步骤进行操作。Workers 作为 QObject 的子类在步骤 1 中创建。在 Workers 中,您可以创建 finished 和 progress 信号。请注意,信号必须创建为类属性。
在步骤 5 中连接以下槽和信号
一旦线程处于活动状态,您必须执行一些重置以使应用程序持续运行。为了防止用户在任务进行时单击“长时间运行任务!”按钮,您可以禁用它。此外,您将线程的 finished 信号链接到一个 lambda 函数,该函数在调用时激活“长时间运行任务!”按钮。长时间运行步骤标签的文本在您的最终连接时重置。 启动此程序后,以下窗口将出现在您的屏幕上 ![]() QRunnable 和 QThreadPool:重用线程如果您的 GUI 应用程序大量依赖多线程,您将遇到与创建和终止线程相关的巨大开销。因此,为了使您的应用程序高效运行,您还需要考虑在特定机器上可以启动多少个线程。值得庆幸的是,PyQt 的线程支持也为您提供了这些问题的解决方案。 每个程序都有一个全局线程池。可以通过调用 QThreadPool.globalInstance() 获取它的引用。 尽管通常使用默认线程池,但 QThreadPool 提供了可重复使用的线程集合,允许您构建自己的线程池。 全局线程池维护和管理建议的线程数量,通常基于当前 CPU 的核心数量。它还负责应用程序中线程的任务排队和执行。由于池中的线程是可重用的,因此不再有创建和删除线程的开销。 您使用 QRunnable 构建任务并在线程池中执行它们。此类象征着必须执行的进程或代码行。生成和执行可运行任务涉及三个过程 通过子类化 QRunnable 来重新实现它。
以二进制兼容任务作为参数,调用 start()。 任务所需的代码必须是 run()。当您调用时,您的任务会在池中可用的一个线程中启动。start()。如果池中没有可用的线程,.start() 会将作业添加到池的运行队列中。.run() 中的代码会在可用的线程中执行。 下面是一个 GUI 程序,演示如何将此过程合并到您的代码中 代码的功能如下
值得注意的是,本教程包含一些与日志记录相关的示例。使用带有简单配置的 info(),消息会打印在屏幕上。这是必要的,因为 print() 不是线程安全函数,可能会导致您的输出混乱。日志例程是线程安全的,允许您在多线程应用程序中使用它们。 如果您使用此应用程序,您将观察到以下行为 ![]() 当您单击“点击我!”按钮时,应用程序最多可以启动四个线程。程序会更新后台终端中每个线程的进度。即使您关闭应用程序,线程也会一直运行,直到它们完成各自的职责。 使用 Python,没有方法可以从外部停止 QRunnable 实例。为了解决这个问题,您可以创建一个全局布尔变量,并在 QRunnable 子类中反复检查它,以便在值为 True 时终止它们。因为 QRunnable 需要更多信号和槽支持,所以在使用 QThreadPool 和 QRunnable 时,线程间通信可能很困难。 然而,QThreadPool 会自动管理线程池,并负责可运行任务的排队和实现。 工作 QThreads 通信如果您正在使用 PyQt 编程多个线程,您可能需要促进应用程序主线程与工作线程之间的通信。这样做可以帮助您将数据传递给线程,获取工作线程状态的反馈,适当地更新 GUI,允许用户暂停执行等等。 PyQt 的信号和槽技术提供了一种可靠且安全的通信方法,用于在 GUI 应用程序中与工作线程进行通信。 相反,您可能还需要在工作线程之间建立通信,例如,通过共享数据缓冲区或任何其他类型的资源。在这种情况下,您必须保护您的数据和资源免受并发访问。 使用槽和信号线程安全对象是可以由多个线程同时访问并保证处于有效状态的对象。由于 PyQt 的槽和信号是线程安全的,因此您可以使用它们在线程之间共享数据并建立线程间通信。 来自一个线程的信号可以连接到另一个线程中的槽。因此,您可以通过在不同的线程中运行代码来响应在一个线程或另一个线程中发出的信号。由此创建了一个安全的线程间通信方式。 因为信号也可以包含数据,如果您发出包含数据的信号,您将在连接到该信号的每个槽中接收到该数据。 在响应式 GUI 应用程序模型中,您利用信号和槽组件来布置字符串之间的对应关系。例如,您将 Workers 的进度信号连接到应用程序的 .reportProgress1() 槽。为了更新长时间运行步骤标签,.reportProgress1() 接受来自 progress 的整数值,该整数值表示长时间运行任务的进度。 PyQt 的线程间通信建立在连接不同线程中的信号和槽的基础之上。此时,尝试使用信号和槽,在响应式 GUI 应用程序中使用 QToolBar 对象而不是长时间运行步骤标签来显示操作的进度。 QMutex(保护共享数据)在多线程 PyQt 应用程序中,QMutex 经常用于防止多个线程同时访问共享资源和数据。您将编写一个图形用户界面 (GUI) 的代码,该界面使用 QMutex 对象来防止对全局变量的并发写入访问。 您将编写一个管理银行账户的示例,其中两个人可以随时取款,以学习如何使用 QMutex。在这种情况下,您需要防止并行访问账户余额。在任何其他情况下,人们可能会取出比他们银行账户中更多的钱。 例如,假设您有一个 100 美元的账户。当两个人同时检查可用余额时,他们发现账户中有 100 美元。他们继续进行交易,因为他们相信他们可以提取 60 美元并在账户中保留 40 美元。账户将有 20 美元的赤字,这可能是一个严重的问题。 编写示例的第一步是导入必要的类、函数和模块。您还定义了两个全局变量并添加了基本日志配置 您将使用全局变量 balance 来跟踪账户的当前余额。您将使用 QMutex 对象 mutex 来保护 balance 免受并发访问。换句话说,互斥锁将阻止多个线程同时访问 balance。 下一步是开发 QObject 的子类,其中包含控制银行账户提款的逻辑。该类将被称为 AccountManager。 然后您定义 .withdraw()。您使用此技术执行以下操作
此应用程序的输出 ![]() 创建上述 GUI 的代码 每次提款时,账户余额都会扣除所需的金额。使用此技术,当前余额标签的文本会更新以反映账户余额的变化。您必须创建两个人并为他们每个人启动一个线程才能完成应用程序 首先,您将实例属性 .threads 添加到 Window 的初始化器中。为了防止线程意外超出其范围,此变量将存储一个列表。从 startThreads() 返回。为了生成两个人并为每个人生成一个线程,您定义 .startThreads()。 您在 .startThreads() 中执行以下操作
您差不多完成了这最后一点代码。 在查看应用程序输出之前,以下是完整的代码以更好地理解。 交易模拟的完整程序示例 如果您从命令行运行此应用程序,那么您将获得以下行为 输出 GUI: Before Withdrawal ![]() After Withdrawal ![]() CLI: Alice Needs to withdraw $60.80... -$60.80 accepted ====Balance====: $39.20 Bob Needs to withdraw $96.90... -$96.90 rejected ====Balance====: $39.20 后台终端上的输出显示线程正在运行。在此示例中,您可以通过使用 QMutex 对象来保护和同步对银行账户余额的访问。因此,防止用户提取比其账户中当前更多的钱。 结论在 PyQt 应用程序主线程上执行的长时间运行活动可能会导致 GUI 无法使用和冻结。这是一个经常出现在功能需求和编码中的问题,可能会导致糟糕的用户体验。通过使用 PyQt 的 QThread 创建的工作线程卸载长时间运行活动,可以在图形应用程序中轻松解决此问题。
下一主题Python 内置异常 |
我们请求您订阅我们的新闻通讯以获取最新更新。