如何通过 moveToThread() 在 pyqt 中正确使用 QThread?

Posted

技术标签:

【中文标题】如何通过 moveToThread() 在 pyqt 中正确使用 QThread?【英文标题】:How to use QThread correctly in pyqt with moveToThread()? 【发布时间】:2013-12-17 22:55:08 【问题描述】:

我读了这篇文章How To Really, Truly Use QThreads; The Full Explanation,它说应该使用 moveToThread 将 QObject 推送到使用 moveToThread(QThread*) 的 QThread 实例上,而不是子类 qthread,并重新实现 run()

这里是 c++ 示例,但我不知道如何将其转换为 python 代码。

class Worker : public QObject
 
     Q_OBJECT
     QThread workerThread;

 public slots:
     void doWork(const QString &parameter) 
         // ...
         emit resultReady(result);
     

 signals:
     void resultReady(const QString &result);
 ;

 class Controller : public QObject
 
     Q_OBJECT
     QThread workerThread;
 public:
     Controller() 
         Worker *worker = new Worker;
         worker->moveToThread(&workerThread);
         connect(workerThread, SIGNAL(finished()), worker, SLOT(deleteLater()));
         connect(this, SIGNAL(operate(QString)), worker, SLOT(doWork(QString)));
         connect(worker, SIGNAL(resultReady(QString)), this, SLOT(handleResults(QString)));
         workerThread.start();
     
     ~Controller() 
         workerThread.quit();
         workerThread.wait();
     
 public slots:
     void handleResults(const QString &);
 signals:
     void operate(const QString &);
 ;



QThread* thread = new QThread;
Worker* worker = new Worker();
worker->moveToThread(thread);
connect(worker, SIGNAL(error(QString)), this, SLOT(errorString(QString)));
connect(thread, SIGNAL(started()), worker, SLOT(process()));
connect(worker, SIGNAL(finished()), thread, SLOT(quit()));
connect(worker, SIGNAL(finished()), worker, SLOT(deleteLater()));
connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
thread->start();

我一直在使用这种方法来生成一个 qthread ,但正如您所见,它使用的是不推荐的方式。我怎样才能重写它以使用首选方法?

class GenericThread(QThread):
    def __init__(self, function, *args, **kwargs):
        QThread.__init__(self)
        # super(GenericThread, self).__init__()

        self.function = function
        self.args = args
        self.kwargs = kwargs

    def __del__(self):
        self.wait()

    def run(self, *args):
        self.function(*self.args, **self.kwargs)

编辑:两年后... 我尝试了 qris 的代码,它可以在不同的线程中工作

import sys
import time
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import pyqtSignal, pyqtSlot
import threading


def logthread(caller):
    print('%-25s: %s, %s,' % (caller, threading.current_thread().name,
                              threading.current_thread().ident))


class MyApp(QtGui.QWidget):

    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)

        self.setGeometry(300, 300, 280, 600)
        self.setWindowTitle('using threads')

        self.layout = QtGui.QVBoxLayout(self)

        self.testButton = QtGui.QPushButton("QThread")
        self.testButton.released.connect(self.test)
        self.listwidget = QtGui.QListWidget(self)

        self.layout.addWidget(self.testButton)
        self.layout.addWidget(self.listwidget)

        self.threadPool = []
        logthread('mainwin.__init__')

    def add(self, text):
        """ Add item to list widget """
        logthread('mainwin.add')
        self.listwidget.addItem(text)
        self.listwidget.sortItems()

    def addBatch(self, text="test", iters=6, delay=0.3):
        """ Add several items to list widget """
        logthread('mainwin.addBatch')
        for i in range(iters):
            time.sleep(delay)  # artificial time delay
            self.add(text+" "+str(i))

    def test(self):
        my_thread = QtCore.QThread()
        my_thread.start()

        # This causes my_worker.run() to eventually execute in my_thread:
        my_worker = GenericWorker(self.addBatch)
        my_worker.moveToThread(my_thread)
        my_worker.start.emit("hello")
        # my_worker.finished.connect(self.xxx)

        self.threadPool.append(my_thread)
        self.my_worker = my_worker


class GenericWorker(QtCore.QObject):

    start = pyqtSignal(str)
    finished = pyqtSignal()

    def __init__(self, function, *args, **kwargs):
        super(GenericWorker, self).__init__()
        logthread('GenericWorker.__init__')
        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.start.connect(self.run)

    @pyqtSlot()
    def run(self, *args, **kwargs):
        logthread('GenericWorker.run')
        self.function(*self.args, **self.kwargs)
        self.finished.emit()


# run
app = QtGui.QApplication(sys.argv)
test = MyApp()
test.show()
app.exec_()

输出是:

mainwin.__init__         : MainThread, 140221684574016,
GenericWorker.__init__   : MainThread, 140221684574016,
GenericWorker.run        : Dummy-1, 140221265458944,
mainwin.addBatch         : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,

【问题讨论】:

我很惊讶这个问题这么长时间都没有得到回答/没有评论。 docs 明确指出使用 moveToThread(thread) 是首选方式,但所有示例代码我都能够找到子类 QThread.run() 并在那里工作。如果我们能看到一个示例或使用模式,那就太好了。 这篇文章对我的第一个线程有很大帮​​助:mayaposch.wordpress.com/2011/11/01/…。正如您所说,即使有很多教程这样做,也不推荐将 QThread 子类化。 问题中的最终样本是否正确? mainwin.add 是代码将项目添加到列表小部件的位置,但从打印输出中可以清楚地看到,mainwin.add 是在线程中执行的?!? 【参考方案1】:

QThread 中默认的 run() 实现会为您运行一个事件循环,相当于:

class GenericThread(QThread):
    def run(self, *args):
        self.exec_()

关于事件循环的重要一点是它允许线程拥有的对象在其插槽上接收事件,这些事件将在该线程中执行。这些对象只是 QObjects,而不是 QThreads。

重要提示:QThread 对象不属于自己的线程 [docs]:

重要的是要记住,QThread 实例存在于实例化它的旧线程中,而不是调用 run() 的新线程中。这意味着 QThread 的所有排队槽和调用的方法都将在旧线程中执行 [例如主线程]。

所以你应该能够做到这一点:

class GenericWorker(QObject):
    def __init__(self, function, *args, **kwargs):
        super(GenericWorker, self).__init__()

        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.start.connect(self.run)

    start = pyqtSignal(str)

    @pyqtSlot()
    def run(self, some_string_arg):
        self.function(*self.args, **self.kwargs)

my_thread = QThread()
my_thread.start()

# This causes my_worker.run() to eventually execute in my_thread:
my_worker = GenericWorker(...)
my_worker.moveToThread(my_thread)
my_worker.start.emit("hello")

另外,请仔细考虑self.function 的结果会发生什么,目前已丢弃。您可以在GenericWorker 上声明另一个信号,它接收结果,并让run() 方法在完成后发出该信号,并将结果传递给它。

一旦你掌握了它的窍门并意识到你没有也不应该继承 QThread,生活就会变得更加简单和轻松。简单地说,永远不要在 QThread 中工作。您几乎不需要覆盖运行。对于大多数用例,将 QObject 与 QThread 建立适当的关联并使用 QT 的信号/插槽创建了一种非常强大的多线程编程方式。请注意不要让您推送到工作线程的 QObjects 徘徊...

http://ilearnstuff.blogspot.co.uk/2012/09/qthread-best-practices-when-qthread.html

【讨论】:

有趣的是,run 方法的 @pyqtSlot 装饰器在这里绝对至关重要;在这种情况下,将其排除在外(在 PyQt 中是“合法的”)将导致看似串行执行。使用 python 2.7.3 测试。 重要提示:如果在方法中创建my_threadmy_worker,请确保它们被存储,否则垃圾收集器会将它们丢弃并且worker 不会运行 确保你不要为你的工作 QObject 分配父级,甚至是 QThread。否则,worker 将在父线程上运行,通常是主线程。您可以通过在主线程和工作函数内部使用threading.get_ident() 来检查哪个线程正在执行工作。 这是正确答案。但是必须修复装饰器才能使其工作:@pyqtSlot()。如果没有括号,这将在 Python 3.9 中给出TypeError: bytes or ASCII string expected not 'function'。对于在课程中使用此代码的任何人:确保存储 my_threadmy_worker,否则它们将被垃圾收集。我还确认没有装饰器,工作线程将在主线程中执行,这违背了整个目的。【参考方案2】:

我试图在我的应用程序中使用 qris 的示例,但我的代码一直在我的主线程中运行!这就是他声明调用run的信号的方式!

基本上,当你在对象的构造函数中连接它时,主线程中的两个对象之间将存在连接——因为QObject的属性属于创建它们的线程。当您将 QObject 移动到新线程时,连接不会随您移动。拿走将信号连接到 run 函数的线路,在将工作线程移动到其新线程后将其连接!

qris 回答的相关变化:

class GenericWorker(QObject):
    def __init__(self, function, *args, **kwargs):
        super(GenericWorker, self).__init__()

        self.function = function
        self.args = args
        self.kwargs = kwargs

    start = pyqtSignal(str)

    @pyqtSlot
    def run(self, some_string_arg):
        self.function(*self.args, **self.kwargs)

my_thread = QThread()
my_thread.start()

# This causes my_worker.run() to eventually execute in my_thread:
my_worker = GenericWorker(...)
my_worker.moveToThread(my_thread)
my_worker.start.connect(my_worker.run) #  <---- Like this instead 
my_worker.start.emit("hello")

【讨论】:

我尝试了@qris 示例代码,它工作正常,我怎么知道他们仍然在同一个步骤中?我用编辑更新了我的问题【参考方案3】:

我已经尝试了@qris 和@MatthewRunchey 两种方法。

使用 @pyqtSlot 装饰器 Qt 在发出信号时检查工作实例的“位置”:即使连接是在 moveToThreadmoveToThread 执行之后发出信号之前建立的工作线程中的插槽。

没有@pyqtSlot装饰器Qt在建立连接的那一刻冻结了worker实例的“位置”:如果它在moveToThread之前,它被绑定到主线程,并且即使在moveToThread 调用之后发出信号,槽代码也会继续在主线程中执行。

之后建立的连接moveToThread 绑定了两种情况下要执行的工作线程的槽。

代码:

import threading
from PyQt5.QtCore import (QCoreApplication, QObject, QRunnable, QThread,
                          QThreadPool, pyqtSignal, pyqtSlot)

class Worker(QObject):
    def __init__(self):
        super(Worker, self).__init__()
#        self.call_f1.connect(self.f1)
#        self.call_f2.connect(self.f2)

    call_f1 = pyqtSignal()
    call_f2 = pyqtSignal()

    @pyqtSlot()
    def f1(self):
        print('f1', threading.get_ident())
    
    @pyqtSlot()
    def f2(self):
        print('f2', threading.get_ident())

app = QCoreApplication([])
print('main', threading.get_ident())
my_thread = QThread()
my_thread.start()

my_worker = Worker()
my_worker.call_f1.connect(my_worker.f1)
my_worker.call_f1.emit()
my_worker.moveToThread(my_thread)
my_worker.call_f2.connect(my_worker.f2)
my_worker.call_f1.emit()
my_worker.call_f2.emit()
sys.exit(app.exec_())

带装饰器:

main 18708
f1 18708
f1 20156
f2 20156

没有装饰器:

main 5520
f1 5520
f1 5520
f2 11472

PS 在worker __init__ 方法中连接显然相当于在主线程中moveToThread 之前连接。

(在 PyQt5、win64 下测试)。

【讨论】:

2.不强制使用装饰器@pyqtSlot,但有好处:riverbankcomputing.com/static/Docs/PyQt5/… @eyllanesc 是的,确实如此。我又做了一些测试。更新答案 如果你使用Qt,千万不要在主线程中使用time.sleep(),最好去掉:time.sleep(0.15) @eyllanesc 是的,我同意。虽然当有一个 GUI 挂起时最重要。再次更新答案。【参考方案4】:

我知道这是一个老问题,但我发现这篇相关且相当新的文章:Use PyQt's QThread to Prevent Freezing GUIs。

它几乎是How To Really, Truly Use QThreads 中给出的方法的最小 Python 实现,它提供了一个非常简洁的示例来说明如何使用 QObject.moveToThread()

我将代码复制并粘贴在这里供参考和讨论(我不是作者):

from PyQt5.QtCore import QObject, QThread, pyqtSignal
# Snip...

# Step 1: Create a worker class class Worker(QObject):
class Worker(QObject):
    finished = pyqtSignal()
    progress = pyqtSignal(int)

    def run(self):
        """Long-running task."""
        for i in range(5):
            sleep(1)
            self.progress.emit(i + 1)
        self.finished.emit()

class Window(QMainWindow):
    # Snip...
    def runLongTask(self):
        # Step 2: Create a QThread object
        self.thread = QThread()
        # Step 3: Create a worker object
        self.worker = Worker()
        # Step 4: Move worker to the thread
        self.worker.moveToThread(self.thread)
        # Step 5: Connect signals and slots
        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.worker.progress.connect(self.reportProgress)
        # Step 6: Start the thread
        self.thread.start()

虽然accepted answer 确实有效,但我认为上述方法非常可取,因为

当工作线程完成时,它会正确地quits 线程, 它使用.deleteLater 对线程和工作线程执行清理,并且 它并不依赖 pyqtSlot() 装饰器在另一个线程中实际运行(我对此进行了测试)。

注意: 在上述方法中,线程——以及连接到thread.started 信号的工作程序——是“手动”启动的,当工作人员发出一个finished 信号时它的工作已经完成。但是,在接受的答案中,worker 是通过start 信号启动的,但没有机制表明它的工作是否完成。

【讨论】:

以上是关于如何通过 moveToThread() 在 pyqt 中正确使用 QThread?的主要内容,如果未能解决你的问题,请参考以下文章

PyQ5t:将 Qt Designer 加载到 Python 脚本中(loadUiType):如何检查错误原因?

如何修复python中opencv中的错误“QObject::moveToThread:”?

qt中通过重写run方法创建线程与通过movetothread方法有啥区别

qt5 通过moveToThread方式创建的线程,主线程(GUI)是不是会控制工作线程

Qt源码阅读 moveToThread

Qt moveToThread 仅在第一次工作