Qt开发中多线程并发QThread子循环需要刷新主循环如何实现?

背景

        最近在开发过程中,遇到了一个问题,我们需要通过点击一个按钮来执行一个耗时操作。若是一般的耗时操作,则进行一个异步设计即可。

        但是此点特殊的情况在于,此耗时操作是个循环,且耗时极长,还需要在每次单个循环结束的时候刷新UI。这就导致异步操作变得不可行。我们只能选择通过创建子线程的方式去达成目标。这本是一个基础的UI开发问题,但是由于小组内缺少有经验的UI开发人员,只能其他开发边学边写。

详细思考与开发过程

首先先对到我手中的代码状态进行下说明,经过几次修正之后,到我手中的代码非常之神奇。阻塞是照阻不误的,刷新居然是隔几分钟刷新一次的,虽然我的算法部分依旧在运行,但是这个UI状态……如何让产品满意?领导可都是产品人员,这不得被diao死?下面是经过我调研理解之后的代码迭代过程:

step1:DEMO设计

在一开始的可行性开发DEMO如下,简单说其实很就是,点击按钮连接到点击槽函数上,槽函数执行单次的耗时操作,此耗时操作使用了Qthread线程摘出去运行。代码示例如下:

class TestWorker(QThread):  
    result_ready = pyqtSignal(bool, int, str)  # 发送结果、行号和测试项文本  
    def __init__(self):  
        super().__init__()   
    
    def run(self):  
        # 执行耗时操作 
        result = func() 
        self.result_ready.emit(result, self.row, self.item_text)

class MainWindow(QWidget):

    '''
    主窗口,用以定义主窗口布局以及总控系统UE、线程
    
    类变量:
        无
    '''
    global result_global

    def __init__(self):
        '''
        初始化类,定义实例变量与窗口初始化
        '''
        super().__init__()
        self.initUI()               # 窗口初始化
        self.current_row = 0
    def on_start_clicked(self):
        # 开始遍历左侧测试内容 从表单第一行开始
        if self.current_row < num:
            worker = TestWorker()  
            worker.result_ready.connect(self.update_widget)
            worker.start()
        else:
            print("所有项目已测试完成!")

    #更新布局
    def update_widget(self):
        '''
        刷新UI界面
        '''
        # 具体的刷新操作……
        self.right_list_widget.show()

    def initUI(self):
        # 创建主垂直布局
        main_vlayout = QVBoxLayout(self)
        # 创建水平布局,包含“开始”按钮
        button_hlayout = QHBoxLayout()
        self.start_button = QPushButton("开始")
        self.start_button.clicked.connect(self.on_start_clicked)
        button_hlayout.addWidget(self.start_button)  
        # 将按钮的水平布局添加到主垂直布局的底部
        main_vlayout.addLayout(button_hlayout)
        # 设置窗口标题和大小
        self.setWindowTitle('test')

if __name__ == '__main__':
    
    print("UI初始化")
    app = QApplication(sys.argv)
    ex = MainWindow()
    ex.show()
    sys.exit(app.exec_())

 step2:加入循环,问题暴露

在上诉的DEMO开发演示过程中,并没有出现什么问题,同事就直接加入了while循环。聚焦于函数如下:

    def on_start_clicked(self):
        # 开始遍历左侧测试内容 从表单第一行开始
        while self.current_row < num:
            worker = TestWorker()  
            worker.result_ready.connect(self.update_widget)
            worker.start()
            self.current_row += 1

有经验的UI开发人员可能一眼就看出了问题……报错信息如下:

QThread: Destroyed while thread is still running。 

解释说明:worker作为临时变量,它生命周期就是单个循环,也就是说,worker.start()、current自增后,worker作为一个变量就结束了,但线程才刚刚开始,所以报错。

step3:尝试解决

直接无脑增加了一个wait……

    def on_start_clicked(self):
        while self.current_row < num:
            worker = TestWorker()  
            worker.result_ready.connect(self.update_widget)
            worker.start()
            worker.wait()
            self.current_row += 1

现在虽然解决了报错,但是……这比没有开线程更差。主线程照样阻塞不说,还平白多了n个线程的创建和销毁过程……而且UI刷新?那是不可能刷新的。主循环已经死在这个while循环里面了,根本不会执行刷新的操作。

step4:神之一手

同事在经过上诉挫折之后,想出了一个神之方案,我到如今都没有理解代码是如何达成这个谜之结果的。具体的思路如下:

  • 按下按钮之后开始执行on_start_clicked,并跳转到start_next_test()执行Qthread线程,
  • 并把Qthread线程的结束信号连接到next_test_finished()函数,
  • 在next_test_finished函数中又开始调用start_next_test……

现象是:阻塞是照阻不误的,刷新居然是隔几分钟也能刷新一次的。代码示例如下:

def on_start_clicked(self):
    '''
    当开始按钮被点击时,开始执行
    '''
    self.start_next_test() 

def start_next_test(self):  
    if self.current_row < num:   
        worker = TestWorker()  
        worker.result_ready.connect(self.update_widget)
        worker.test_finished.connect(self.next_test_finished)
        worker.start()
        worker.wait()
        self.current_row += 1
def next_test_finished(self):
    self.start_next_test()

step5:代码重构 

到在下手中的时候就是上诉的状态,没想到我一个算法开发人员还需要进行UI的开发工作……

没办法,直接开始重构吧。

设定固定刷新机制

对于Qt的UI刷新机制,主循环会不断监听各种事件的发生,并响应。 所以针对上诉UI刷新相关的问题,我直接在主循环中每100ms提交一次UI刷新任务,在ui_init中运行即可。

def Mytimer(self):
    timer = QTimer(self)
    timer.timeout.connect(self.update_widget)
    timer.start(100)

while循环全部放入Qthread中去

上诉代码最大的问题就是while循环放在了主循环中运行,使得主线程直接阻塞了,这才是根本原因,若不更正这个问题,后续所有的补救措施都是没用的。因为我们需要在子线程中更新主线程的UI,我直接设定了一个共享变量resultlist,Qthread线程实时的去更新该变量,而主线程也根据该变量每100ms刷新一次UI。

Qthread线程如下:

class TestWorker(QThread):  
    result_ready = pyqtSignal(bool, int, str)  # 发送结果、行号和测试项文本  
    
    def __init__(self, num):  
        super().__init__()  
        self.num = num
    
    def run(self):
        global current_row
        global test_cases
        # 执行自动化脚本   
        while current_row < self.num:
            result = func()
            resultlist[current_row] = str(result)
            current_row += 1 

修改主线程UI刷新函数 

如下图for循环所示,根据resultlist共享变量来刷新UI。注:update_widget就是上面Mytime函数链接的函数

    def update_widget(self):
        '''
        刷新UI界面
        '''
        #刷新
        for i,text in enumerate(resultlist):
            item = self.right_list_widget.item(i)
            item.setText(text)
            if text == "False":
                self.right_list_widget.item(i).setForeground(QBrush(Qt.darkRed))
            else:
                self.right_list_widget.item(i).setForeground(QBrush(Qt.darkGreen))
        self.right_list_widget.show() 

结尾

说实话,小组成员是第一次操作Qt中的Qthread模块,也是第一次对Qt UE进行编写,错漏难免,小生在解决上诉问题之后,记笔记到此,增进经验。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千天夜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值