使用PyQt线程的正确姿势

用了Python一段时间了,图形编程看了一些,还是觉得PyQt比较方便,主要得益于designer和uic两个工具,使得前端页面可视编程,也方便转换为代码。关于这两个工具的使用网上一大堆,我觉得并没有必要重复,只有有一点要提醒大家注意,就是用uic生成的程序文件千万别动!!!新写一个类继承它,需要增加的方法都放在子类里面,这样我们重新修改ui文件,再用uic生成代码的时候就不用再修改太多(如果不修改已有的控件名就不用修改)。

说到图形界面,就避开不了线程,毕竟UI主线程必须保证事件循环不被阻塞来响应用户的输入。一开始使用PyQt的时候,程序动不动就未响应,后来才知道是因为把需要长时间运行的代码放在了主线程,阻塞了事件循环。这时候,我们便需要把这部分代码移到其他线程,通过信号与槽来实现线程间的通信。所谓线程,按照我目前的理解,是包含了自己的事件循环机制,所谓事件循环,也就是说当线程接收到信号的时候,如果有响应的槽,可以做出响应。同时,线程还可以发送信号给其他线程,信号可以带参数。

由于写博客的时间有点仓储,本博客里面的代码全部是截取我最近的项目里面的代码,但跟博客内容有关的部分我会尽量呈现和说明清楚,望见谅。

QtCore.QThread是一个管理线程的类,当我们使用其构造函数的时候,便新建了一个线程。这里要强调,QThread是一个线程管理器,不要把业务逻辑放在这个类里面,Qt的作者已经多次批评继承QThread类来实现业务逻辑的做法。那么,我们怎样把代码放到Thread中运行呢?答案如下:

self.writing_thread = QtCore.QThread()
self.writer.moveToThread(self.writing_thread)
我们把业务逻辑写在一个QtCore.QObject的子类里面,然后新建一个实例,例如上述代码的writer,然后调用继承了父类的方法moveToThread,这样就可以把该对象放在线程里面。剩下的工作就是通过信号和槽的机制处理业务逻辑和返回结果了。一般来说,槽函数所在的类在哪个线程,这个函数就在哪个线程执行。

新建一个signal:

start_to_think_signal = QtCore.pyqtSignal(int, str)
这里要注意一点,首先,我不喜欢把常量放在最外层,一般的习惯是哪个线程的信号,哪个类的信号就放回在哪个类,而且要作为类变量定义,必须写在init函数前,具体原因也不理解,应该跟预读类def有关。例如:

class MyApp(QtWidgets.QMainWindow, generator.Ui_writing):
    # signal一定要在init前,具体原因不清楚
    start_to_think_signal = QtCore.pyqtSignal(int, str)

    def __init__(self):

接着定义槽函数,槽函数是可以带参数的,参数的个数就跟新建信号时传入的类型参数一致,顺序和类型也保持一致,除了成员函数的self参数不用传。

    def think(self, num, prime):
        model = lstm.CharLSTM(lstm_size=MyInit.hyper_para('lstm_size'), lstm_layers=MyInit.hyper_para('lstm_layers'),
                              grad_clip=MyInit.hyper_para('grad_clip'))
        for char, prediction in model.generate(checkpoint_path=MyInit.get_dir_path('checkpoint_home'), len_sample=num,
                                               top_n=5, prime=prime):
            probability = np.squeeze(prediction)
            char_probability = ''
            for index in np.argsort(-probability)[:30]:
                potential_char = model.preparation.index_to_char[index]
                if potential_char == '\n':
                    potential_char = '\\n'
                char_probability += '%s: %.2f%%\n' % (potential_char, probability[index] * 100)
            self.update_think_signal.emit(char_probability)
            self.update_perplexity_signal.emit(float(compute_perplexity(probability)))
            time.sleep(0.5)
            self.update_write_signal.emit(char)
        self.stop_thread_signal.emit()

然后我们就可以开始连接信号和槽,具体什么时候连接就看你定义,语句如下:

self.writer.update_write_signal.connect(self.write)
self.writer.update_think_signal.connect(self.think)
self.writer.update_perplexity_signal.connect(self.output_perplexity)
self.writer.stop_thread_signal.connect(self.stop_thread)

self.start_to_think_signal.connect(self.writer.think)
其中self(MyApp的实例)是在主线程,self.writer是在次线程,这里说一下为什么writer要作为主函数类的成员函数,主要是因为这个对象是在MyApp中的方法里面创建的,如果不存为类的成员,很可能就在方法结束的时候被释放了,这时候就会报错。如果不希望寄托在这个类里面,一定要保证存这个对象的变量是个全局变量,至少生命周期要覆盖你使用它的周期。

把需要连接的信号和槽连接好了以后,就可以把线程运行了:

self.writing_thread.start()
线程开始后,便开始了事件循环,我们使用

self.start_to_think_signal.emit(self.num_to_write.value(), self.prime_input.toPlainText())
来把信号发出去,从代码可见,信号里面传了两个参数过去,被writer.think()接收,然后代码开始运行。我在writer.think()里面发出了若干个信号,将会被主线程接收并执行主线程的代码,其中需要注意的是:

self.stop_thread_signal.emit()
这个代码在次线程中被调用,用来告诉主线程可以关闭次线程,为什么要这样做?首先,线程是不会自己关掉的,如果我们不关线程而结束进程,就会出现异常。第二,线程管理器QThread是在主线程的,因此需要传一个signal,告诉主线程调用下面的方法:

    def stop_thread(self):
        self.writing_thread.quit()
这样就可以关闭线程了。

跟线程相关的简单使用就介绍到这里,上述的内容并不涉及线程锁,因为所有处理逻辑都是在类的内部,参数都通过信号传递,没有全局变量,而如果有全局变量,还涉及加锁的问题,这个以后再讨论。但建议不要过多使用线程锁,那样还是会造成线程间阻塞,起不到异步的目的。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页