什么是线程与多线程:线程有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
为什么要使用多线程而不是进程:线程在程序中是独立的、并发的执行流。创建线程相比创建进程开销更小更快,同时线程之间可以很方便的共享内存、文件句柄和其他进程应有的状态。
我们主要利用多线程做什么:通常情况下,我们使用多线程在同一时间完成我们设置的不同任务,比如测试过程中,我们既要发送CAN数据,又要监听接口或摄像头捕捉到的信息。
接下来,我们主要通过一些实例,来说明如何创建线程,以及其包含的参数。
首先,如下是,创建线程的方法:
import threading
def play_song(file):
for n in range(3):
print(file)
sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=play_song, name='aa', args=("aa",), daemon=False)
#t1是创建线程后产生的句柄
#target后,需要指定线程中需要运行的函数,
#name代表形成本身的名称,
#arg=填写的是需要传入函数中的参数
#daemon, 代表如果main主函数结束时,如果当前线程还没有执行完成,则程序会继续等待线程执行完成还是立即停止。
t1.start()
#线程,必须通过start()来启动
t2 = threading.Thread(target=play_song, name='bb', args=("bb",), daemon=False)
t2.start()
sleep(1)
执行后,打印的结果为,程序同时执行了两个函数的任务:
aa
bb
aa
bb
aa
bb
这里简单提下Thread中的daemon参数,以上的例子中当我们把daemon设置为True时,再来看看程序发生了什么变化。
if __name__ == '__main__':
t1 = threading.Thread(target=play_song, name='aa', args=("aa",), daemon=True)
# daemon=True
t1.start()
t2 = threading.Thread(target=play_song, name='bb', args=("bb",), daemon=True)
# daemon=True
t2.start()
sleep(1)
执行后,打印的结果如下:
aa
bb
从结果看,程序执行了1s就结束了,这是为什么哪?
原因就在于,daemon是守护的意思, 当daemon赋值为True时, 程序不会等到线程执行结束, 只要主程序一结束, 线程也会结束; 当daemon赋值为False时, 进程会等线程执行结束再退出,创建线程时daemon的默认值是False的。
接着,我们再来说说join()函数,同样还是以上的例子,我们如果在最后加上了.join(),看看程序会发生什么变化。
if __name__ == '__main__':
t1 = threading.Thread(target=play_song, name='aa', args=("aa",), daemon=True)
t1.start()
t2 = threading.Thread(target=play_song, name='bb', args=("bb",), daemon=True)
t2.start()
sleep(1)
#添加了join()
t1.join()
t2.join()
执行后,打印的结果如下,程序同时执行完了两个函数的任务:
aa
bb
aa
bb
aa
bb
这里一定会有个疑问,为什么我们设置 daemon=True后,程序还是等待了所有线程的任务都执行完成后才退出哪?
原因就在于我们使用了.join()函数, 使用.join()后,程序便会阻塞住,直到对应线程执行完成后,才跳到下一步。
随后,我们来说说is_alive(), 它的作用,主要用于查询线程的状态,是激活状态还是非激活状态。
if __name__ == '__main__':
t1 = threading.Thread(target=play_song, name='aa', args=("aa",), daemon=True)
t1.start()
t2 = threading.Thread(target=play_song, name='bb', args=("bb",), daemon=True)
t2.start()
#打印线程的运行状态
print(t1.is_alive())
print(t2.is_alive())
t1.join()
t2.join()
#打印线程的运行状态
print(t1.is_alive())
print(t2.is_alive())
执行结果如下:
aa
bb
True
True
aa
bb
bb
aa
False
False
从执行结果,我们能看出线程执行过程和执行完成后,状态的变化。
这里有一点也要明确,Python中线程是没有停止的概念的,官方并没有针对线程提供停止函数,线程的停止取决与程序自身的生命周期。
所以,我们可以思考下,如果我们在一个线程中,使用了一个while循环,让线程持续运行某个函数体,那么要停止这个线程的方式有哪些?
- 可以通过,daemon=True通过主程序结束来让线程也结束。
- 也可在while体内,添加共享变量,触发程序跳出while循环。
- 也可以通过threading下的event对象来触发停止while循环。(我们会在后续的教程中介绍其使用方法)
- 上网搜索会得知有一些通过异步函数来强制停止线程的方法(这里不推荐,原因就在于,官方希望线程的结束是完成某个任务后自然的退出)。
接着,我们再来看看,线程之间共享变量的方法。
Python中线程间共享变量的类型分为几种:
第一种:可变类型(列表、字典、可变集合),这类要共享的是可变类型参数,直接将该参数当作实参通过[args]传入线程中去,便可以实现变量间的共享。
如:
import threading
from time import sleep
def demo1(a, num):
sleep(0.5)
# 为a增加元素
for i in range(num):
a.append(i)
print('demo1的a:{}'.format(a))
def demo2(a, num):
sleep(0.5)
# 为a增加元素
for i in range(num):
a.append(i * 2)
print('demo2的a:{}'.format(a))
if __name__ == '__main__':
# 创建两个参数
a = [11, 22, 33]
num = 8
# 创建两个线程,并将两个参数传递给指定的函数
threading.Thread(target=demo1, args=(a, num)).start()
threading.Thread(target=demo2, args=(a, num)).start()
sleep(1)
print('主线程的a:{}'.format(a))
执行结果如下:
demo1的a:[11, 22, 33, 0, 1, 2, 3, 4, 5, 6, 7]
demo2的a:[11, 22, 33, 0, 1, 2, 3, 4, 5, 6, 7, 0, 2, 4, 6, 8, 10, 12, 14]
主线程的a:[11, 22, 33, 0, 1, 2, 3, 4, 5, 6, 7, 0, 2, 4, 6, 8, 10, 12, 14]
从结果我们看出,变量list a, 即使没有特别声明,在两个线程中也是完成共享读写。
第二种:不可变类型(数字、字符串、元组、不可变集合),要共享的是不可变类型参数,不能直接将该参数当作实参通过args传入线程中去,需要在进程执行前创建不可变类型的参数,并且在线程中对其进行修改时需要申明global 全局变量。
如:
def demo1(num):
'''a+100'''
sleep(0.5)
for i in range(num):
global a #需要声明为全局变量
a += 1
print('demo1的a:{}'.format(a))
def demo2(num):
'''a+100'''
sleep(0.5)
for i in range(num):
global a #需要声明为全局变量
a += 1
print('demo2的a:{}'.format(a))
if __name__ == '__main__':
global a #需要声明为全局变量
a = 0
# 创建一个参数
num = 100
# 创建两个线程,并将参数传递给指定的函数
threading.Thread(target=demo1, args=(num,)).start()
threading.Thread(target=demo2, args=(num,)).start()
sleep(1)
print('主线程的a:{}'.format(a))
执行结果如下:
demo1的a:100
demo2的a:200
主线程的a:200
可以看到,添加了global 声明后,两个线程,才拥有了共享此变量的读写能力,这里也建议多线程读写变量时,尽量添加之前介绍的lock功能,以保证修改不被干扰。
第三种共享变量为Queue, 它是Python标准库中的线程安全的队列(FIFO)实现, 提供了一个适用于多线程编程的先进先出的数据结构,即队列,用来在生产者和消费者线程之间的信息传递。
下面是实例:
import threading
from time import sleep
import queue
q = queue.Queue()
def demo1(num):
for i in range(num):
sleep(0.2)
q.put(num) #将内容放入队列中
def demo2(num):
for i in range(num):
sleep(0.2)
q.put(num) #将内容放入队列中
if __name__ == '__main__':
# 创建两个线程,并将参数传递给指定的函数
threading.Thread(target=demo1, args=(1,)).start() #创建+启动线程一行写法
threading.Thread(target=demo2, args=(2,)).start()
sleep(2)
print('主线程的a:{}'.format(list(q.queue))) #打印队列的所有内容
while not q.empty():
print(q.get()) #一一取出队列中的内容
执行后,我们发现两个线程同样拥有读写此队列的能力。
同时,Queue因为其线程安全的属性(不需要添加lock来保护其读写),比较推荐用于生产者和消费者线程之间的信息传递(如:测试程序里,一个生产者线程用于接收CAN报文,一个消费者线程用于解析和判断接收的CAN报文)。
总结,Python中除非需要同时执行大量含计算的任务(如:算圆周率之类),都建议使用线程。原因就在于其共享变量方便,开销小。而且长远看官方也会尽力释放对线程的性能限制,让其更快。
线程共享变量建议使用list dict queue 等可变类型共享变量,这样不必做过多的声明,且性能更优。