如何将Python函数/方法和PyQt/PySide槽转移到独立的线程上(附代码示例)

199 阅读5分钟

在PyQt 5.15.0和PySide 6.2.0版本中,QThreadPool.start() 方法被扩展为除了只接受一个Python函数、一个Python方法或一个PyQt/PySide,还可以接受一个QRunnable 对象。这简化了在后台运行Python代码的过程,避免了为每个任务创建一个QRunnable 对象的麻烦。

关于为多线程创建QRunnable 对象的更多信息,请参阅多线程教程

.start() 方法将一个函数/方法/槽的执行安排在一个单独的线程上,使用QThreadPool ,所以它避免了阻塞你的应用程序的主GUI线程。因此,如果你有一个或多个长期运行的任务需要完成或在后台运行,将它们传递给.start() ,就可以了。

我们将建立一个简单的演示应用程序,模拟一个长期运行的任务,以展示.start() 如何将一个用户定义的Python函数/方法或PyQt/PySide槽转移到一个单独的线程上。

但首先,让我们从一个有缺陷的方法开始。

阻断GUI

我们的演示程序是一个从1开始向上计数的绵羊计数器。在这个过程中,你可以按一个按钮来挑选一只绵羊。而由于挑选一只羊是很难的,所以需要一些时间来完成。

Demo app screenshot 这就是我们的演示应用程序的样子。

确保你使用的是PyQt 5.15.0+或PySide 6.2.0+;否则,演示程序将不能为你工作:

  • PySide6
  • PyQt6
  • PyQt5
import time

from PySide6.QtCore import Slot, QTimer
from PySide6.QtWidgets import (
    QLabel,
    QWidget,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QApplication,
)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setFixedSize(250, 100)
        self.setWindowTitle("Sheep Picker")

        self.sheep_number = 1
        self.timer = QTimer()
        self.picked_sheep_label = QLabel()
        self.counted_sheep_label = QLabel()

        self.layout = QVBoxLayout()
        self.main_widget = QWidget()
        self.pick_sheep_button = QPushButton("Pick a sheep!")

        self.layout.addWidget(self.counted_sheep_label)
        self.layout.addWidget(self.pick_sheep_button)
        self.layout.addWidget(self.picked_sheep_label)

        self.main_widget.setLayout(self.layout)
        self.setCentralWidget(self.main_widget)

        self.timer.timeout.connect(self.count_sheep)
        self.pick_sheep_button.pressed.connect(self.pick_sheep)

        self.timer.start()

    @Slot()
    def count_sheep(self):
        self.sheep_number += 1
        self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")

    @Slot()
    def pick_sheep(self):
        self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
        time.sleep(5)  # This function represents a long-running task!


if __name__ == "__main__":
    app = QApplication([])

    main_window = MainWindow()
    main_window.show()

    app.exec()
import time

from PyQt6.QtCore import pyqtSlot, QTimer
from PyQt6.QtWidgets import (
    QLabel,
    QWidget,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QApplication,
)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setFixedSize(250, 100)
        self.setWindowTitle("Sheep Picker")

        self.sheep_number = 1
        self.timer = QTimer()
        self.picked_sheep_label = QLabel()
        self.counted_sheep_label = QLabel()

        self.layout = QVBoxLayout()
        self.main_widget = QWidget()
        self.pick_sheep_button = QPushButton("Pick a sheep!")

        self.layout.addWidget(self.counted_sheep_label)
        self.layout.addWidget(self.pick_sheep_button)
        self.layout.addWidget(self.picked_sheep_label)

        self.main_widget.setLayout(self.layout)
        self.setCentralWidget(self.main_widget)

        self.timer.timeout.connect(self.count_sheep)
        self.pick_sheep_button.pressed.connect(self.pick_sheep)

        self.timer.start()

    @pyqtSlot()
    def count_sheep(self):
        self.sheep_number += 1
        self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")

    @pyqtSlot()
    def pick_sheep(self):
        self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
        time.sleep(5)  # This function represents a long-running task!


if __name__ == "__main__":
    app = QApplication([])

    main_window = MainWindow()
    main_window.show()

    app.exec()
import time

from PyQt5.QtCore import pyqtSlot, QTimer
from PyQt5.QtWidgets import (
    QLabel,
    QWidget,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QApplication,
)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setFixedSize(250, 100)
        self.setWindowTitle("Sheep Picker")

        self.sheep_number = 1
        self.timer = QTimer()
        self.picked_sheep_label = QLabel()
        self.counted_sheep_label = QLabel()

        self.layout = QVBoxLayout()
        self.main_widget = QWidget()
        self.pick_sheep_button = QPushButton("Pick a sheep!")

        self.layout.addWidget(self.counted_sheep_label)
        self.layout.addWidget(self.pick_sheep_button)
        self.layout.addWidget(self.picked_sheep_label)

        self.main_widget.setLayout(self.layout)
        self.setCentralWidget(self.main_widget)

        self.timer.timeout.connect(self.count_sheep)
        self.pick_sheep_button.pressed.connect(self.pick_sheep)

        self.timer.start()

    @pyqtSlot()
    def count_sheep(self):
        self.sheep_number += 1
        self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")

    @pyqtSlot()
    def pick_sheep(self):
        self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
        time.sleep(5)  # This function represents a long-running task!


if __name__ == "__main__":
    app = QApplication([])

    main_window = MainWindow()
    main_window.show()

    app.exec()

当你运行演示程序并按下 ***挑选一只羊!***按钮时,你会注意到有5秒钟,GUI是完全没有反应的。这可不好。

GUI响应性的延迟来自于行time.sleep(5) ,它使Python代码的执行暂停了5秒。这是为模拟一个长期运行的任务而添加的。然而,我们可以通过线程来改善这个问题,你将在后面看到。

请随意试验,增加延迟的长度--将一个大于5的数字传给.sleep() --你可能会注意到你的操作系统开始抱怨演示应用程序没有响应。

在一个单独的线程上运行一个任务

那么,我们怎样才能提高我们的演示应用程序的响应速度呢?这就是QThreadPool 的扩展.start() 方法的用处!

首先,我们需要导入QThreadPool ,所以让我们来做这件事:

  • PySide6
  • PyQt6
  • PyQt5
from PySide6.QtCore import QThreadPool
from PyQt6.QtCore import QThreadPool
from PyQt5.QtCore import QThreadPool

接下来,我们需要创建一个QThreadPool 实例。让我们把

self.thread_manager = QThreadPool()

MainWindow 类的__init__ 块中。

现在,让我们创建一个pick_sheep_safely() 槽。它将使用.start() 方法来调用长期运行的pick_sheep() 槽,并将其从GUI主线程移到一个单独的线程上:

  • PySide
  • PyQt
@Slot()
def pick_sheep_safely(self):
    self.thread_manager.start(self.pick_sheep)  # This is where the magic happens!
@pyqtSlot()
def pick_sheep_safely(self):
    self.thread_manager.start(self.pick_sheep)  # This is where the magic happens!

此外,确保你将pick_sheep_safely() 槽与self.pick_sheep_buttonpressed 信号连接起来。因此,在MainWindow 类的__init__ 块中,你应该有

self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)

而如果你一直跟着,我们改进后的演示应用程序的代码现在应该是:

  • PySide6
  • PyQt6
  • PyQt5
import time

from PySide6.QtCore import Slot, QThreadPool, QTimer
from PySide6.QtWidgets import (
    QLabel,
    QWidget,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QApplication,
)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setFixedSize(250, 100)
        self.setWindowTitle("Sheep Picker")

        self.sheep_number = 1
        self.timer = QTimer()
        self.picked_sheep_label = QLabel()
        self.counted_sheep_label = QLabel()

        self.layout = QVBoxLayout()
        self.main_widget = QWidget()
        self.thread_manager = QThreadPool()
        self.pick_sheep_button = QPushButton("Pick a sheep!")

        self.layout.addWidget(self.counted_sheep_label)
        self.layout.addWidget(self.pick_sheep_button)
        self.layout.addWidget(self.picked_sheep_label)

        self.main_widget.setLayout(self.layout)
        self.setCentralWidget(self.main_widget)

        self.timer.timeout.connect(self.count_sheep)
        self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)

        self.timer.start()

    @Slot()
    def count_sheep(self):
        self.sheep_number += 1
        self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")

    @Slot()
    def pick_sheep(self):
        self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
        time.sleep(5)  # This function doesn't affect GUI responsiveness anymore...

    @Slot()
    def pick_sheep_safely(self):
        self.thread_manager.start(self.pick_sheep)  # ...since .start() is used!


if __name__ == "__main__":
    app = QApplication([])

    main_window = MainWindow()
    main_window.show()

    app.exec()
import time

from PyQt6.QtCore import pyqtSlot, QThreadPool, QTimer
from PyQt6.QtWidgets import (
    QLabel,
    QWidget,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QApplication,
)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setFixedSize(250, 100)
        self.setWindowTitle("Sheep Picker")

        self.sheep_number = 1
        self.timer = QTimer()
        self.picked_sheep_label = QLabel()
        self.counted_sheep_label = QLabel()

        self.layout = QVBoxLayout()
        self.main_widget = QWidget()
        self.thread_manager = QThreadPool()
        self.pick_sheep_button = QPushButton("Pick a sheep!")

        self.layout.addWidget(self.counted_sheep_label)
        self.layout.addWidget(self.pick_sheep_button)
        self.layout.addWidget(self.picked_sheep_label)

        self.main_widget.setLayout(self.layout)
        self.setCentralWidget(self.main_widget)

        self.timer.timeout.connect(self.count_sheep)
        self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)

        self.timer.start()

    @pyqtSlot()
    def count_sheep(self):
        self.sheep_number += 1
        self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")

    @pyqtSlot()
    def pick_sheep(self):
        self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
        time.sleep(5)  # This function doesn't affect GUI responsiveness anymore...

    @pyqtSlot()
    def pick_sheep_safely(self):
        self.thread_manager.start(self.pick_sheep)  # ...since .start() is used!


if __name__ == "__main__":
    app = QApplication([])

    main_window = MainWindow()
    main_window.show()

    app.exec()
import time

from PyQt5.QtCore import pyqtSlot, QThreadPool, QTimer
from PyQt5.QtWidgets import (
    QLabel,
    QWidget,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QApplication,
)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setFixedSize(250, 100)
        self.setWindowTitle("Sheep Picker")

        self.sheep_number = 1
        self.timer = QTimer()
        self.picked_sheep_label = QLabel()
        self.counted_sheep_label = QLabel()

        self.layout = QVBoxLayout()
        self.main_widget = QWidget()
        self.thread_manager = QThreadPool()
        self.pick_sheep_button = QPushButton("Pick a sheep!")

        self.layout.addWidget(self.counted_sheep_label)
        self.layout.addWidget(self.pick_sheep_button)
        self.layout.addWidget(self.picked_sheep_label)

        self.main_widget.setLayout(self.layout)
        self.setCentralWidget(self.main_widget)

        self.timer.timeout.connect(self.count_sheep)
        self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)

        self.timer.start()

    @pyqtSlot()
    def count_sheep(self):
        self.sheep_number += 1
        self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")

    @pyqtSlot()
    def pick_sheep(self):
        self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
        time.sleep(5)  # This function doesn't affect GUI responsiveness anymore...

    @pyqtSlot()
    def pick_sheep_safely(self):
        self.thread_manager.start(self.pick_sheep)  # ...since .start() is used!


if __name__ == "__main__":
    app = QApplication([])

    main_window = MainWindow()
    main_window.show()

    app.exec()

当你按下 选一只羊!按钮时,pick_sheep() 槽会在一个单独的线程上执行,不再阻塞GUI的主线程。绵羊计数继续进行,GUI保持响应--尽管我们的演示应用程序仍然要在后台完成一个长期运行的任务。

现在试着增加延迟的长度--例如,time.sleep(10) --并注意到它不再影响GUI了。

结论

就这样吧!.start() 我希望你会发现QThreadPool 的扩展方法在你的PyQt/PySide GUI应用中很有帮助,因为这些应用有长期运行的任务要在后台执行。