Python 中多线程的应用

什么是线程与多线程:线程有时被称为轻量进程(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循环,让线程持续运行某个函数体,那么要停止这个线程的方式有哪些?

  1. 可以通过,daemon=True通过主程序结束来让线程也结束。
  2. 也可在while体内,添加共享变量,触发程序跳出while循环。
  3. 也可以通过threading下的event对象来触发停止while循环。(我们会在后续的教程中介绍其使用方法)
  4. 上网搜索会得知有一些通过异步函数来强制停止线程的方法(这里不推荐,原因就在于,官方希望线程的结束是完成某个任务后自然的退出)。

接着,我们再来看看,线程之间共享变量的方法。
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 等可变类型共享变量,这样不必做过多的声明,且性能更优。

你可能感兴趣的:(Python 中多线程的应用)