为什么 PySide6 应用程序在 QThreadPool 中执行多个 QRunnables 时将鼠标悬停在自定义按钮上时崩溃

Why PySide6 application crashes when hovering over custom button while executing several QRunnables in QThreadPool

提问人:chris1656 提问时间:11/15/2023 最后编辑:chris1656 更新时间:11/20/2023 访问量:34

问:

我在 Stackoverflow 上的第一个问题:我会尽力解释这个问题;

我目前正在使用基于 PySide6 的 Python 应用程序。 有几个耗时的计算作为实例运行。到目前为止,一切正常。QRunnableQThreadPool.globalInstance()

我还对 UI 进行了修改。现在的问题是,当我在后台运行时将鼠标悬停在按钮上时,应用程序崩溃了。QPushButtonQRunnables

我创建了一个最小的可重复示例。一个非常简单的工作线程 (),它只等待 1 秒,完成后发出 finishQRunnableSignal

文件q_worker.py

from PySide6.QtCore import QObject, QRunnable, Signal, Slot
from time import sleep

class WorkerSignals(QObject):
finished = Signal()

class WorkerSleep(QRunnable):

    def __init__(self):
        super(WorkerSleep, self).__init__()
        self.signals = WorkerSignals()
    
    @Slot()
    def run(self):
        sleep(1)
        self.signals.finished.emit()

在中找到仅覆盖方法的类。此外,将创建一个带有 1 和 1 的窗口。main.pyCustomButtonpaintEventCustomButtonQLabel

文件 main.py

import sys
import os

from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton
from PySide6.QtCore import Qt, QThreadPool, QTimer, Slot
from PySide6.QtGui import QPainter, QPaintEvent, QLinearGradient

from q_worker import WorkerSleep

class CustomButton(QPushButton):
    def __init__(self, parent=None):
        super(CustomButton, self).__init__(parent)
        self.setFixedSize(650, 99)

    def paintEvent(self, event: QPaintEvent):
        fill_color = QLinearGradient.Preset.TemptingAzure

        with QPainter(self) as p:
            p.setRenderHint(QPainter.RenderHint.Antialiasing)
            rect = event.rect()
            p.fillRect(rect, fill_color)


class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        # set window title
        self.setWindowTitle('minimal reproducible example')

        # time in seconds, before workers are created
        self._count = 5

        # number of workers
        self._num_workers_to_create = 100
        self._num_workers_completed = 0

        self._setup_ui()
        self.show()

        # start countdown to create workers
        self._timer = QTimer(self)
        self._timer.timeout.connect(self._countdown)
        self._timer.start(1000)
    
    def _setup_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        central_widget_layout = QVBoxLayout(central_widget)
    
        # add ui elements
        self.label_counter = QLabel(f"<span style= font-size:25pt>{self._count}</span>")
        central_widget_layout.addWidget(self.label_counter, alignment=Qt.AlignmentFlag.AlignHCenter)
        central_widget_layout.addWidget(CustomButton(parent=self))
    
    @Slot()
    def _countdown(self):
        self._count -= 1
        self.label_counter.setText(f"<span style= font-size:25pt>{self._count}</span>")
    
        if self._count == 0:
            self._timer.stop()
            self._create_workers()
    
    def _create_workers(self):
        for i in range(self._num_workers_to_create):
            worker = WorkerSleep()
            worker.signals.finished.connect(self._update_completed_workers)
            QThreadPool.globalInstance().start(worker)
    
    @Slot()
    def _update_completed_workers(self):
        num_threads = str(QThreadPool.globalInstance().activeThreadCount())
        self._num_workers_completed += 1
        self.label_counter.setText(f"<span style= font-size:25pt>"
                                   f"{self._num_workers_completed} Workers completed "
                                   f"({num_threads} Threads running)</span>")

def main():
app = QApplication(sys.argv)
mw = MainWindow()
app.exec()

if __name__ == "__main__":
sys.exit(main())

运行该示例时,窗口打开,计时器倒计时 5 秒,然后在 中启动 100 次。只要我不将鼠标悬停在按钮上,一切正常。但是,当我在处理工作线程时将鼠标悬停在按钮上时,应用程序崩溃了。WorkerSleepQThreadPool.globalInstance()

起初我以为是因为添加的太多了。但是,根据文档,当到达时,它们将被排队。这似乎可以完美地工作,因为默认值始终为 8。QRunnablesQThreadPool.globalInstance()QThreadPool.maxThreadCount()QThreadPool.globalInstance().activeThreadCount()

经过进一步尝试后,问题似乎出在 .因为如果使用标准,一切都会完美运行。CustomButtonQPushButton

因此,如果我删除自定义,一切都按预期工作。所以这种方法似乎有问题。但是无论我尝试对这种方法进行什么更改,它都会崩溃。paintEvent

最后但并非最不重要的一点是,还有另一个我无法解释的超级奇怪的现象:当我用较小的尺寸初始化按钮时

class CustomButton(QPushButton):
    def __init__(self, parent=None):
        super(CustomButton, self).__init__(parent)
        self.setFixedSize(650, 50)

该应用程序在我和我的自定义下运行良好。CustomButtonpaintEvent

我正在运行 Windows 10、Python 3.9.9PySide6 v.6.4.1


我尝试了以下方法:

  • 添加到没有解决问题setAutoDelete(False)WorkerSleep.__init__
  • 在 venv 中从 (Windows) 控制台运行会导致相同的结果
  • 我还尝试了使用新 venv 的完全不同的 Windows 机器 - 从控制台运行会导致相同的结果
  • 我将 PySide6 版本升级到当前的 v.6.6.0 并查看此处 - 然后它起作用了

我无法解决 Python 3.9.9 和 PySide 6.4.1 的问题?

并发 pyside6 pyqt6 paintevent qrunnable

评论

0赞 ekhumoro 11/16/2023
我根本无法在使用 Qt-6.6.0 和 PySide-6.6.0 的 arch-linux 上重现这一点。请编辑您的问题,并提供您正在测试的平台以及您正在使用的 Qt6 和 PySide6 特定版本的详细信息。如果您使用的是 IDE,请改为在普通控制台/命令窗口中测试脚本。您还应该尝试使用 PyQt6 版本的脚本重现该问题。
0赞 musicamante 11/16/2023
@ekhumoro我也无法在虚拟机上使用最新的 Qt5 和 6 重现它,但我可以在具有较旧 Qt5 版本的物理机上重现它。这可能与线程池在处理信号时试图销毁 QRunnable 的方式有关:添加对 worker 的引用似乎并不能解决它,但会解决。Qt5 中的私有发生了变化(解锁了在该上下文中创建的互斥锁),但由于它也存在于 Qt6 中,我看不出这将如何关联(而且我对多线程的了解不足以做到这一点)。setAutoDelete(False)::clear()
0赞 musicamante 11/16/2023
@chris1656 你能检查一下,如果你添加到你的可运行子类中,是否有一些变化?self.setAutoDelete(False)__init__
0赞 musicamante 11/16/2023
@chris1656 另外,你能解释一下你在这些跑步者中实际做了什么吗?使用 QRunnable 可能不是正确的方法,也许您只需要一个系统来管理一些持久的 QThreads,并最终使用队列为它们分配任务。
0赞 musicamante 11/17/2023
@chris1656好的,但这并不能真正解释问题。另请注意,在终端/提示符中运行脚本的建议不一定会给出不同的行为,但它旨在最终获得不同的输出(某些 IDE 并不总是能够做到这一点)。此外,设置可能不够,您可能还需要将实例添加到持久性列表中。我知道您现在可能已经解决了这个问题,但对于无法使用您的版本(甚至找不到不相关的错误)的人来说,这仍然是一个问题。不要只是安顿下来,而是要进行调查。autoDelete()worker

答: 暂无答案