在Python3中,推荐使用threading而不是thread,原thread已废(通过import _thread可兼容访问,但不推荐使用,毕竟官方抛弃了。。。)

在编写UI界面时,必然会遇到多线程问题:比如背景循环刷数据,通讯帧处理等。在PyQT中,提供了QThread类,在讲这个类之前,先看Python原生的线程类及其使用

Python线程类

  • 先看一段简单的代码
import threading
import time

class myThread(threading.Thread):
    def __init__(self,threadID,name,counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter

    def run(self):
        print('start thread' + self.name + time.ctime(time.time()))
        print_time(self.name,self.counter)
        print('exit time'+ self.name + time.ctime(time.time()))

def print_time(threadName,counter):
    while counter:
        time.sleep(1)
        print(threadName,time.ctime(time.time()))
        counter -= 1

if __name__ == "__main__":
    thread1 = myThread(1,'thread1',2)
    thread2 = myThread(2,'thread2',4)

    thread1.start()
    thread2.start()
    print('exit main' + time.ctime(time.time()))
  • 执行结果
Connected to pydev debugger (build 183.4284.139)
start threadthread1Mon Dec  3 19:38:25 2018
start threadthread2Mon Dec  3 19:38:25 2018
exit mainMon Dec  3 19:38:25 2018
thread1 Mon Dec  3 19:38:26 2018
thread2 Mon Dec  3 19:38:26 2018
thread1 Mon Dec  3 19:38:27 2018
exit timethread1Mon Dec  3 19:38:27 2018
thread2 Mon Dec  3 19:38:27 2018
thread2 Mon Dec  3 19:38:28 2018
thread2 Mon Dec  3 19:38:29 2018
exit timethread2Mon Dec  3 19:38:29 2018

Process finished with exit code -1
  • 代码说明

0.首先要说的是,从if __name__ == "__main__":开始,是主线程的开始
.
1.myThread类继承自threading类,包含构造函数和run函数,
.
2.构造函数用于内部参数初始化,run函数用于启动线程
.
3.main函数新建两个myThread实例,分别执行对象的start方法,启动线程(调用run)
.
4.着重是run函数,run函数循环执行print_time,次数由counter指定,计数结束即退出线程。因此,如果需要该线程一直执行,应该在run内部设置死循环,比如刷新界面时间

  • LOG说明
    在19:38:25的时候,“同时”打印出三条log分别来自主线程、子线程1和子线程2,主线程结束,还剩下两个子线程;随后,线程1 run函数跑2次,线程2 run函数跑4次,每个线程内部都会时延1秒,跑完即退出

发现问题

主线程并没有等子线程结束,就自行退出了,我们如果想主线程大哥最后撤退,做一些收尾工作,该怎么办?

  • join的引入
    基于上面的问题,引入了join

join ()方法:主线程中,创建了子线程a,并且在主线程中调用了a.join(),那么,主线程会在调用的地方等待,直到子线程a执行完成后,再接着往下执行。

原型:join([timeout])
timeout参数是可选的,代表该线程运行的最大时间。即如果超过这个时间,主线程就不等了,自己先走了。最后就剩那些超时的线程在那里墨迹,最后一个线程墨迹完了,程序也就结束了。当然,你可以说大哥必须等,那timeout缺省就行了,大哥一直等到大家忙完,再忙自己的。
可以以上面的代码举例:
在线程开启后,添加join

thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()

输出结果:

Connected to pydev debugger (build 183.4284.139)
start threadthread1Mon Dec  3 20:04:26 2018
start threadthread2Mon Dec  3 20:04:26 2018
thread1 Mon Dec  3 20:04:27 2018
thread2 Mon Dec  3 20:04:27 2018
thread1 Mon Dec  3 20:04:28 2018
exit timethread1Mon Dec  3 20:04:28 2018
thread2 Mon Dec  3 20:04:28 2018
thread2 Mon Dec  3 20:04:29 2018
thread2 Mon Dec  3 20:04:30 2018
exit timethread2Mon Dec  3 20:04:30 2018
exit mainMon Dec  3 20:04:30 2018

Process finished with exit code -1

可以看到,大哥主线程一直在等子线程1 子线程2执行完毕才执行,最后打印exit mainMon Dec 3 20:04:30 2018退出
继续举这个例子,把thread1.join()改成thread1.join(1),即主线程只等你1秒,还等不到就先跑路了
看运行结果

Connected to pydev debugger (build 183.4284.139)
start threadthread1Mon Dec  3 20:10:26 2018
start threadthread2Mon Dec  3 20:10:26 2018
thread2 Mon Dec  3 20:10:27 2018
thread1 Mon Dec  3 20:10:27 2018
thread1 Mon Dec  3 20:10:28 2018
exit timethread1Mon Dec  3 20:10:28 2018
thread2 Mon Dec  3 20:10:28 2018
thread2 Mon Dec  3 20:10:29 2018
thread2 Mon Dec  3 20:10:30 2018
exit timethread2Mon Dec  3 20:10:30 2018
exit mainMon Dec  3 20:10:30 2018

Process finished with exit code -1

可是,主线程还是最后才退出,这是为什么?因为,虽然子线程1总共要运行2s,主线程只等你1s,但主线程还要等子线程2,而且是无限等。子线程2执行4s,然后主线程开始执行,此时子线程1早已收工。因此,最后是主线程输出。
所以,如果把thread2.join()改成thread2.join(1),输出log如下:

Connected to pydev debugger (build 183.4284.139)
start threadthread1Mon Dec  3 20:16:54 2018
start threadthread2Mon Dec  3 20:16:54 2018
thread2 Mon Dec  3 20:16:55 2018
thread1 Mon Dec  3 20:16:55 2018
thread1 Mon Dec  3 20:16:56 2018
exit timethread1Mon Dec  3 20:16:56 2018
thread2 Mon Dec  3 20:16:56 2018
exit mainMon Dec  3 20:16:57 2018
thread2 Mon Dec  3 20:16:57 2018
thread2 Mon Dec  3 20:16:58 2018
exit timethread2Mon Dec  3 20:16:58 2018

Process finished with exit code 0

主线程无限等子线程1结束,再等子线程2,最多等1s还墨迹也不等了,主线程先撤。最后子线程2忙完了,程序也就结束了。

  • join到底是什么

join的原理就是依次检验线程池中的线程是否结束,没有结束就阻塞主线程直到子线程结束。如果子线程结束,则执行下一个线程的join函数

  • join特点
  1. 阻塞主进程,多线程多join的情况下,依次执行各线程的join方法,前头一个结束了才能执行后面一个。
  2. 参数timeout为线程的阻塞时间,若缺省则为无限等,若赋参数则为主线程等待时间,一旦超时就不等了,继续执行下面的代码

还有问题

如果从程序设计的角度看,主线程应该是程序的核心,主线程结束了,子线程也应该相继结束,这该如何实现?

  • 引入setDaemon

当我们使用setDaemon(True)方法,设置子线程为守护线程时,主线程一旦执行结束,则子线程全部被终止执行

但是,也会带来负面影响:可能出现的情况是,子线程的任务还没有完全执行完毕,就被迫停止

  • 再谈join

join有一个timeout参数:
如前所述:
如果没有设置子线程为守护线程时,主线程会等待子线程timeout时间,超时了,主线程往下执行直至结束。但是并没有杀死子线程,子线程依然可以继续执行,直到子线程全部结束,程序退出。
如果设置子线程为守护线程时,含义是主线程对于子线程等待timeout的时间将会杀死该子线程,最后退出程序。总结来看:时间一到,不管子线程有没有完成,直接杀死。

以刚刚最后的情形举例:

import threading
import time

class myThread(threading.Thread):
    def __init__(self,threadID,name,counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter

    def run(self):
        print('start thread' + self.name + time.ctime(time.time()))
        print_time(self.name,self.counter)
        print('exit time'+ self.name + time.ctime(time.time()))

def print_time(threadName,counter):
    while counter:
        time.sleep(1)
        print(threadName,time.ctime(time.time()))
        counter -= 1

if __name__ == "__main__":
    thread1 = myThread(1,'thread1',2)
    thread2 = myThread(2,'thread2',4)

    thread1.setDaemon(True)
    thread2.setDaemon(True)

    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join(1)

    print('exit main ' + time.ctime(time.time()))

输出结果:

Connected to pydev debugger (build 183.4284.139)
start threadthread1Mon Dec  3 20:56:42 2018
start threadthread2Mon Dec  3 20:56:42 2018
thread2 Mon Dec  3 20:56:43 2018
thread1 Mon Dec  3 20:56:43 2018
thread1 Mon Dec  3 20:56:44 2018
exit timethread1Mon Dec  3 20:56:44 2018
thread2 Mon Dec  3 20:56:44 2018
exit main Mon Dec  3 20:56:45 2018

Process finished with exit code 0

主线程只等子线程2一秒,还在墨迹,直接kill掉,然后退出


到这里,Python的多线程算是告一段落,下面讲一下PyQT的多线程应用

PyQT中的多线程QThread

通常,我们会通过继承QThread,重写run方法,来实现业务需求
比如:

class MBThread(QThread):
    oneSecondTriger = pyqtSignal()

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

    def run(self):
        while True:
            self.oneSecondTriger.emit()
            time.sleep(1)

我定义了一个MBThread类,继承自QThread,在run函数内,每隔1s触发一个信号
而在另一个类MyWindow中,我定义了MBThread的实例,并绑定了槽函数。代码如下:

class MyWindow(QMainWindow,Ui_MainWindow):
    def __init__(self):
        super(MyWindow,self).__init__()
        self.timeThread = MBThread()
        self.timeThread.oneSecondTriger.connect(self.timeUpdate)
        self.timeThread.start()

    def timeUpdate(self):
        self.label_Time.setText(time.strftime('%H:%M:%S'))

        try:
            x = self.MBusMaster.execute(35, cst.READ_HOLDING_REGISTERS, 4, 2)
            self.rh = AddrIntToFloat(x[0], x[1])
            x = self.MBusMaster.execute(35, cst.READ_HOLDING_REGISTERS, 5, 2)
            self.t = AddrIntToFloat(x[0], x[1])
        except modbus_tk.modbus_rtu.ModbusInvalidResponseError as err:
            print(err)

        self.label_RHValue.setText(str(round(self.rh,2)))
        self.label_TValue.setText(str(round(self.t,2)))

为了方便阅读,上段代码做了一些删除,能表达清楚意思即可。槽函数timeUpdate在新的类中MyWindow定义,每次信号触发都将调用该函数。同样地,UI当中的时间标签label_Time也会在每次触发调用后,更新界面时间。

多余的话:上述代码来自我的一个小项目,应用了Python的Modbus库,在线程槽函数timeUpdate内,改成你需要的业务处理就行了。

好,本篇结束