如何重新启动在QThread内运行的Worker。

huangapple go评论65阅读模式
英文:

PyQt How to restart a Worker that is running inside a QThread

问题

标题类似于这个:https://stackoverflow.com/questions/71407687/how-to-restart-a-function-that-is-running-inside-a-qthread,但由于没有答案,我将用我的代码提出问题:

# 以下为您提供的 Python 代码,我将对其进行分析并解释您的问题。

from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QGridLayout,
    QWidget,
)
import sys
import time

class AThread(QThread):
    restart_worker = pyqtSignal()

    def __init__(self):
        super(AThread, self).__init__()

    def restarted(self):
        self.restart_worker.emit()

class Go(QMainWindow):
    # 其他部分被省略

    def thread_start(self):
        # 其他部分被省略
        if self.thread.isRunning() == False:
            self.thread.start()
            print('self.thread.isRunning() :', self.thread.isRunning())
        else:
            self.thread.restarted()
            self.thread.start()

    # 其他部分被省略

class Worker(QObject):
    # 其他部分被省略

if __name__ == '__main__':
     app = QApplication(sys.argv)
     MainApp = Go()
     MainApp.show()
     sys.exit(app.exec_())

您的代码主要涉及使用PyQt5来创建一个多线程应用程序,其中有一个主窗口类(Go),一个工作线程类(Worker),以及一个子线程类(AThread)。主要问题似乎是您希望能够重新启动工作线程,而不必重新创建新的QThread。

在您的代码中,self.thread.restarted() 方法被调用来发射一个信号,这个信号被连接到了工作线程的 self.worker.run 方法。这是一个可行的方法,但似乎您对此感到困惑。您还提到了一个问题,即为什么您不能使用简单的变量来控制工作线程的重新启动。

首先,关于为什么您不能使用简单的变量来重新启动工作线程的问题:在Python中,线程一旦被启动,就不能再次启动。这就是为什么在您的代码中,当工作线程已经在运行时,您需要发射一个信号来请求重新启动它。这是因为您不能简单地再次调用 self.thread.start() 来重新启动一个已经运行的线程。

至于是否这是一个正确的方法,取决于您的需求。如果您需要能够在工作线程运行时重新启动它,那么发射一个信号并连接到工作线程的 run 方法可能是一种合理的方式。这种方法允许您在工作线程执行完毕后重新启动它。

然而,如果您发现这种方法令您感到困惑或者觉得它不够清晰,您可以考虑重新设计您的应用程序逻辑,以避免需要重新启动工作线程。这可能需要更深入地理解您的需求和设计更复杂的状态管理系统,但这取决于您的具体情况和偏好。

英文:

Title is similat to this : https://stackoverflow.com/questions/71407687/how-to-restart-a-function-that-is-running-inside-a-qthread but since there is no answer I'll ask with my code:

from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot

from PyQt5.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QGridLayout,
    QWidget,
)

import sys
import time


class AThread(QThread):
    restart_worker = pyqtSignal()

    def __init__(self):
        super(AThread, self).__init__()


    def restarted(self):
        
        self.restart_worker.emit()


class Go(QMainWindow):
    custom_signal = pyqtSignal(str)
    button_signal = pyqtSignal(str, str)

    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        
        
        self.control =  {'break': False} # dict with your control variable
        
        self.flag = 'off'
        
        self.setupUi()
        
        print('runing Main  on Qthread : ' , int(QThread.currentThreadId()) )
        
    def setupUi(self):
        self.setWindowTitle("Main GUI")
        self.resize(300, 250)
        
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        
        # self.thread = QThread()
        
        self.thread = AThread()

        self.worker = Worker(self.control, self.flag) 
        
        self.worker.moveToThread(self.thread)
        
        self.label_thread_worker = QLabel("thread_worker: 0")
        
        self.start_thread_worker_button = QPushButton("start_thread_worker", self)
        
        self.flag_var_button = QPushButton("flag_is_variable", self)
        
        self.flag_var_button.setDisabled(True)
        
        self.flag_dict_button = QPushButton("flag_is_dict", self)
        
        self.flag_dict_button.setDisabled(True)
        
        # self.start_thread_worker_button.clicked.connect(self.thread.start)
        
        self.thread.restart_worker.connect(self.worker.run)
        
        self.thread.started.connect(self.worker.run)
        
        self.start_thread_worker_button.clicked.connect(self.thread_start)
        
        self.worker.progress.connect(self.reportProgress)
        
        self.flag_dict_button.clicked.connect(self.change_flag_dict)
        
        self.flag_var_button.clicked.connect(self.change_flag_var)
        
        self.worker.finished.connect(self.stop)
        
        # Set the layout
        layout = QGridLayout()     
        layout.addWidget(self.label_thread_worker, 2, 1)
        layout.addWidget(self.start_thread_worker_button, 2, 2)
        layout.addWidget(self.flag_var_button, 3,2)        
        layout.addWidget(self.flag_dict_button, 4,2)        
        self.centralWidget.setLayout(layout)
    
    
    def thread_start(self):
        
        print('self.thread.isRunning() :', self.thread.isRunning())
        if self.thread.isRunning() == False:
            
            self.thread.start()
            print('self.thread.isRunning() :', self.thread.isRunning())
            
        else:
            
            self.thread.restarted()
            
            self.thread.start()            
            
    
    def reportProgress(self, n):
        self.label_thread_worker.setText(f"thread_worker: {n}")
        
        self.start_thread_worker_button.setDisabled(1)
        
        self.flag_var_button.setDisabled(0)
        
        self.flag_dict_button.setDisabled(0)
        
    def change_flag_dict(self):
        self.control['break'] = True
        
    def change_flag_var(self):
        self.flag = 'on'
        print('self.flag = ', self.flag)
    
    
    @pyqtSlot(str)
    def stop(self, sentence):
        print('stopping worker with : ', sentence)
        
        self.label_thread_worker.setText("thread_worker: 0")
        self.start_thread_worker_button.setEnabled(1)
        
        self.flag_var_button.setDisabled(True)
        
        self.flag_dict_button.setDisabled(True)
        
        
        self.control['break'] = False
        
        # self.close()
        
   
class Worker(QObject):
    progress = pyqtSignal(int)
    
    finished = pyqtSignal(str)
    
    
    def __init__(self, ctrl, flag, parent=None):
        super(Worker, self).__init__(parent)
             
        self.control = ctrl
        self.flag = flag 
        self.sentence = ''
    
    
    @pyqtSlot()
    def run(self):
               
        print('running worker on Qthread : ' , int(QThread.currentThreadId()) )
        
        self.cnt = 0
        while True:
            
            time.sleep(1)
            
            self.cnt += 1
            
            self.progress.emit(self.cnt)
            
            # print(self.flag)
            
            if self.control['break'] == True:
                
                self.sentence = 'flag_is_dict'
                
                break
            
            if self.flag == 'on' :
                
                self.sentence = 'flag_is_variable'
                
                break
        
        self.finished.emit(self.sentence)
            
            
if __name__ == '__main__':
     app = QApplication(sys.argv)
     MainApp = Go()
     MainApp.show()
     sys.exit(app.exec_())

This code was a Minimal Reproducible Example that I wrote to ask another question abouy how/why I can stop a Worker inside a QThread passing a Mutable flag to it but cannot do the same with a simply Variable, I know I read somewhere that this is the way but being naive at Python, I cannot figure out/picture in my mind why the latter doesn't work like the former. While creating the MRE I realized I wanted to be able to restart my Worker without re-creating a new QThread. Here the problem started. As you can see from my code :

def thread_start(self):
        
        print('self.thread.isRunning() :', self.thread.isRunning())
        if self.thread.isRunning() == False:
            
            self.thread.start()
            print('self.thread.isRunning() :', self.thread.isRunning())
            
        else:
            
            self.thread.restarted()
            
            self.thread.start()      


If I comment out self.thread.restarted() my Worker cannot be restarted anymore.

I though I could call self.thread.start() to have my Worker running again as per

self.thread.started.connect(self.worker.run) but had to end up subcclassing QThread with:

class AThread(QThread):
    restart_worker = pyqtSignal()

    def __init__(self):
        super(AThread, self).__init__()


    def restarted(self):
        
        self.restart_worker.emit()

and using self.thread.restart_worker.connect(self.worker.run) in my QMainWindow

init toghether with a call to self.thread.restarted() [see code above].

Why is that ? What am I doing wrong ?

Code works as follow :

pressing "start_thread_worker" button --> thread_workercounter starts counting

pressing flag_is_variable --> nothing happens (this is my question about mutable flag)

pressing flag_is_dict ---> thread_worker counter stops

I can only restart start_thread_worker using the subclassed QThread with new Signal/Slot
called by self.thread.restarted().

Side note:

@pyqtSlot() decorator in class Worker(QObject): is not needed for the example to work

Of course using self.thread = QThread() with self.thread.start() in def thread_start(self): wasnt working so I tried with the subclassing.

EDITED

Re reading-it looks like a very dumb question !!! How could you start something that is already started ???

So I'll try changing my question in : is it the right/best way to accomplish my desiderata or is something you should not do it and I should reimplement the entire logic of my problem/task

答案1

得分: 2

以下是您要翻译的内容:

The reason that you think that your thread is not reacting to changing the variable is due to some incorrect assumptions. In python, strings are immutable objects. Which means that when two variables reference the same string, reassigning one of the variables to a different string will have no effect on the other variable.

So in your main window you assign self.flag = "off" and then you pass that variable to your worker and assign the same str instance with self.flag = flag. However in your change_flag_var method, when you reassign the self.flag variable to "on", this has no effect on the self.flag variable that belongs to the Worker class.

To demonstrate.

>>> flag = "on"
>>> flag2 = flag
>>> flag is flag2
True
>>> flag2
'on'
# Now we reassign flag to "off" and it will have no effect on flag2
>>> flag = "off"
>>> flag
'off'
>>> flag2
'on'
>>> flag is flag2
False

This can be further demonstrated by simply changing your change_flag_var method to the following example and it will work as you originally expected it too, and actually break the while loop of the worker class.:

def change_flag_var(self):
    self.worker.flag = 'on'
    print('self.worker.flag = ', self.worker.flag)

For the question regarding restarting the worker's run method, I see nothing wrong with the way you have implemented it. However, at least with this example, personally I would probably just convert the Worker class into a subclass of QThread and simply spin up a new instance every time the start_worker_thread_button is clicked. For example:

class Go(QMainWindow):
    custom_signal = pyqtSignal(str)
    button_signal = pyqtSignal(str, str)

    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self.setupUi()
        self.flag = "off"
        print('runing Main  on Qthread : ', int(QThread.currentThreadId()) )

    def setupUi(self):
        self.setWindowTitle("Main GUI")
        self.resize(300, 250)
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        self.label_thread_worker = QLabel("thread_worker: 0")
        self.start_thread_worker_button = QPushButton("start_thread_worker", self)
        self.flag_var_button = QPushButton("flag_is_variable", self)
        self.flag_var_button.setDisabled(True)
        self.start_thread_worker_button.clicked.connect(self.thread_start)
        self.flag_var_button.clicked.connect(self.change_flag_var)
        # Set the layout
        layout = QGridLayout()
        layout.addWidget(self.label_thread_worker, 2, 1)
        layout.addWidget(self.start_thread_worker_button, 2, 2)
        layout.addWidget(self.flag_var_button, 3,2)
        self.centralWidget.setLayout(layout)


    def thread_start(self):
        self.worker = Worker(self.flag)
        self.worker.finished.connect(self.stop)
        self.worker.finished.connect(self.worker.deleteLater)
        self.worker.progress.connect(self.reportProgress)
        self.worker.start()

    def reportProgress(self, n):
        self.label_thread_worker.setText(f"thread_worker: {n}")
        self.start_thread_worker_button.setDisabled(1)
        self.flag_var_button.setDisabled(0)

    def change_flag_var(self):
        self.worker.flag = 'on'
        print('self.flag = ', self.worker.flag)

    def stop(self):
        self.label_thread_worker.setText("thread_worker: 0")
        self.start_thread_worker_button.setEnabled(1)
        self.flag_var_button.setDisabled(True)


class Worker(QThread):
    progress = pyqtSignal(int)

    def __init__(self, flag, parent=None):
        super().__init__(parent)
        self.flag = flag
        self.sentence = ''

    def run(self):
        print('running worker on Qthread : ', int(QThread.currentThreadId()) )
        self.cnt = 0
        while True:
            time.sleep(1)
            self.cnt += 1
            self.progress.emit(self.cnt)
            if self.flag == 'on' :
                self.sentence = 'flag_is_variable'
                break
英文:

The reason that you think that your thread is not reacting to changing the variable is due to some incorrect assumptions. In python, strings are immutable objects. Which means that when two variables reference the same string, reassigning one of the variables to a different string will have no effect on the other variable.

So in your main window you assign self.flag = "off" and then you pass that variable to your worker and assign the same str instance with self.flag = flag. However in your change_flag_var method, when you reassign the self.flag variable to "on", this has no effect on the self.flag variable that belongs to the Worker class.

To demonstrate.

>>> flag = "on"
>>> flag2 = flag
>>> flag is flag2
True
>>> flag2
'on'
# Now we reassign flag to "off" and it will have no effect on flag2
>>> flag = "off"
>>> flag
'off'
>>> flag2
'on'
>>> flag is flag2
False

This can be further demonstrated by simply changing your change_flag_var method to the following example and it will work as you originally expected it too, and actually break the while loop of the worker class.:

def change_flag_var(self):
    self.worker.flag = 'on'
    print('self.worker.flag = ', self.worker.flag)

For the question regarding restarting the worker's run method, I see nothing wrong with the way you have implemented it. However, at least with this example, personally I would probably just convert the Worker class into a subclass of QThread and simply spin up a new instance every time the start_worker_thread_button is clicked. For example:

class Go(QMainWindow):
    custom_signal = pyqtSignal(str)
    button_signal = pyqtSignal(str, str)

    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self.setupUi()
        self.flag = "off"
        print('runing Main  on Qthread : ' , int(QThread.currentThreadId()) )

    def setupUi(self):
        self.setWindowTitle("Main GUI")
        self.resize(300, 250)
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        self.label_thread_worker = QLabel("thread_worker: 0")
        self.start_thread_worker_button = QPushButton("start_thread_worker", self)
        self.flag_var_button = QPushButton("flag_is_variable", self)
        self.flag_var_button.setDisabled(True)
        self.start_thread_worker_button.clicked.connect(self.thread_start)
        self.flag_var_button.clicked.connect(self.change_flag_var)
        # Set the layout
        layout = QGridLayout()
        layout.addWidget(self.label_thread_worker, 2, 1)
        layout.addWidget(self.start_thread_worker_button, 2, 2)
        layout.addWidget(self.flag_var_button, 3,2)
        self.centralWidget.setLayout(layout)


    def thread_start(self):
        self.worker = Worker(self.flag)
        self.worker.finished.connect(self.stop)
        self.worker.finished.connect(self.worker.deleteLater)
        self.worker.progress.connect(self.reportProgress)
        self.worker.start()

    def reportProgress(self, n):
        self.label_thread_worker.setText(f"thread_worker: {n}")
        self.start_thread_worker_button.setDisabled(1)
        self.flag_var_button.setDisabled(0)

    def change_flag_var(self):
        self.worker.flag = 'on'
        print('self.flag = ', self.worker.flag)

    def stop(self):
        self.label_thread_worker.setText("thread_worker: 0")
        self.start_thread_worker_button.setEnabled(1)
        self.flag_var_button.setDisabled(True)


class Worker(QThread):
    progress = pyqtSignal(int)

    def __init__(self, flag, parent=None):
        super().__init__(parent)
        self.flag = flag
        self.sentence = ''

    def run(self):
        print('running worker on Qthread : ' , int(QThread.currentThreadId()) )
        self.cnt = 0
        while True:
            time.sleep(1)
            self.cnt += 1
            self.progress.emit(self.cnt)
            if self.flag == 'on' :
                self.sentence = 'flag_is_variable'
                break

huangapple
  • 本文由 发表于 2023年5月29日 04:30:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/76353484.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定