簡體   English   中英

如何通過 moveToThread() 在 pyqt 中正確使用 QThread?

[英]How to use QThread correctly in pyqt with moveToThread()?

我閱讀了這篇文章如何真正、真正地使用 QThreads; 完整的解釋,它說而不是子類 qthread,並重新實現 run(),應該使用 moveToThread 將 QObject 推送到使用 moveToThread(QThread*) 的 QThread 實例

這是 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,

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

我試圖在我的應用程序中使用 qris 的示例,但我的代碼一直在我的主線程中運行! 這就是他宣布調用run的信號的方式!

基本上,當您在對象的構造函數中連接它時,主線程中的兩個對象之間將存在連接——因為 QObject 的屬性屬於創建它們的線程 當您將 QObject 移動到新線程時,連接不會隨您移動 拿走將信號連接到 run 函數的線,並在將 worker 移動到其新線程后將其連接!

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 和@MatthewRunchey 方法。

使用@pyqtSlot裝飾器,Qt 在發出信號時檢查工作實例的“位置”:即使在moveToThread在工作線程中執行槽之后moveToThread信號之前建立了連接。

如果沒有@pyqtSlot裝飾器,Qt 會在建立連接的那一刻凍結工作實例的“位置”:如果它在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 下測試)。

我知道這是一個老問題,但我發現這篇相關且相當新的文章: 使用 PyQt 的 QThread 防止凍結 GUI

它幾乎是如何真正、真正使用 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()

雖然接受的答案確實有效,但我認為上述方法非常可取,因為

  • 工作人員完成后,它會正確quit線程,
  • 它使用.deleteLater對線程和工作線程執行清理,並且
  • 它不依賴pyqtSlot()裝飾器在另一個線程中實際運行(我對此進行了測試)。

注意:在上述方法中,線程——以及連接到thread.started信號的工作程序——是“手動”啟動的,工作人員在其工作完成時發出finished信號。 然而,在接受的答案中,工人是通過start信號啟動的,但沒有機制表明其工作是否完成。

我正在測試Anthony 的答案,並注意到即使在 f1 或 f2 方法中定義變量名也無法獲得發射值。 它給出了“ TypeError: Worker.f1() missing 1 required positional argument: 'variable' ” 錯誤。花了 1 個小時才注意到您需要確定發送到插槽的數據類型。 我只是在這里提醒誰在復制粘貼代碼並為此苦苦掙扎..

import threading,sys
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(object)<--- add type of data you want to get
    call_f2 = pyqtSignal()

    @pyqtSlot(object)  <--- add type of data you want to get
    def f1(self,variable):
        print('f1',variable, threading.get_ident()) <-- now you can get it
    
    @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.moveToThread(my_thread)
my_worker.call_f1.emit("test data") <--- Now you can add your data to emit 
my_worker.call_f2.connect(my_worker.f2)
my_worker.call_f1.emit()
my_worker.call_f2.emit()
sys.exit(app.exec_())

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM