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 等可变类型共享变量,这样不必做过多的声明,且性能更优。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容