十六 多任务
一 多任务介绍
(1) 多任务概念
所谓多任务就是同一时刻执行多件事情,就是多个任务同时执行。
1 生活中的多任务
- 手舞足蹈
- 手脚并用
- 眼观六路耳听八方
2 计算机中的多任务
现代计算机中都有很多软件,我们开启电脑后可以在电脑上同时运行多个软件,我们可以一边听着歌曲一边写代码等。但是我们认为的多个软件同时执行,真正也是同时执行吗?我们需要了解下计算机执行任务的原理。
(2) 计算机多任务原理
计算机中所有的任务都是CPU帮助我们是执行的,由于CPU执行代码都是顺序执行的,当计算机为单核CPU时,操作系统会轮流让各个任务交替执行,假设任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。实际上,每个任务都是交替执行的,并不是同一时刻有多个任务执行,但是,由于CPU的执行和切换速度实在是太快了,普通用户感觉就像所有任务都在同时执行一样,也就是说你以为的同时进行并不是真正的同时进行。
如果是多核计算机,那么意味着同一时刻确实可以执行多个任务,每个CPU负责一个任务,但是一台现代计算机的任务数会大于CPU核数,所以在真正电脑启动起来之后,多个任务之间还会有切换和暂停。
上述CPU轮流执行这种方式有个名词:时间片轮转法,每个任务执行的单位时间叫做时间片。
补充:
- 并发:同一时刻,总有任务在等待中
- 并行:同一时刻,有多个任务在执行
(3) python中实现多任务的方式
- 进程(了解)
- 线程(重点)
- 协程(暂不介绍)
(4) 多任务好处
- 提高用户体验
- 提高CPU利用效率
- 提高程序执行效率等
二 进程和线程
(一) 进程
首先需要明白程序和进程区别
1 程序
平时写的代码或是py文件就是程序,是一个静态概念,只要我们不运行一个程序,程序就不会自己执行
2 进程
一个程序运行起来之后,这个程序和运行这个程序所需要的资源的统称称为进程。资源包括内存资源,CPU资源等,进程是操作系统分配资源的基本单位。
在我们windows的任务管理器中可以看到进程
注意:一个程序在运行起来之后可能会开启多个进程,比如上图中的Chrome浏览器,在打开浏览器之后,Chrome开启了多个进程。
3 进程特点
- 进程是资源拥有者,是操作系统分配资源的基本单位,开启多进程相对会耗费更多的资源
- 进程能够充分利用多核CPU优势,适用于CPU/计算密集型任务
(二)线程
1线程理解
由于开启多进程相对比较耗费资源,线程是位于进程内部的子任务,一个进程可以拥有1个或多个线程,所以开启多线程方式会大大解决资源的消耗。
多个线程之间共享进程资源,线程是CPU调用的基本单位。
2 线程特点
- 相比进程,资源开销更小,是CPU调度的基本单位
- 不能充分利用多核资源,适合于IO密集型任务
(三)进程和线程关系
- 一个进程至少有一个线程
- 多个线程共享进程资源
- 进程之间资源不共享
在Windows任务管理器中可以发现一个进程有多个线程,甚至是很多
(三) 线程实现多任务
python内部内置了threading模块的Thread类,帮助我们便捷开启多线程。
1 使用threading模块创建线程
1 Thread类中参数
Thread(group=None, target=None, name=None, args=(), kwargs=None, *,daemon=None)
属性参数介绍
- group:组,一般不用
- target:子线程执行的任务,一般为函数
- args:如果target需要位置参数,那么通过元组的形式传入
- kwargs:如果target需要字典参数,通过字典方式传入
- name:线程设定名字,可以不设定
- daemon:是否设置守护线程,只能传入布尔值
2 Thread中常用方法
- start():启动子线程
- join([timeout]):主线程等待子线程执行结束,或等待多少秒,默认一直等到子线程结束在继续执行
- getName:获取线程的名字
- setName:设置线程的名字
- is_alive()/isAlive():判断线程是否活着
- isDaemon():判断线程是否为守护线程
- setDaemon():设置当前线程为守护线程
3 案例演示
1> start()和target
# 多任务演示载歌载舞
import time
from threading import Thread
def sing():
"""唱歌"""
print("我来为唱歌")
time.sleep(0.2) # 添加时间为了演示,否则程序运行时间太短
print("两只老虎,两只老虎跑得快")
time.sleep(0.2)
print("一直没有尾巴")
time.sleep(0.2)
print("一直没有耳朵")
time.sleep(0.2)
print("真奇怪,真奇怪!")
def dance():
"""跳舞"""
print("跳舞动作:摇头")
time.sleep(0.2)
print("跳舞动作:挥手")
time.sleep(0.2)
print("跳舞动作:扭腰")
time.sleep(0.2)
print("跳舞动作:动脚")
if __name__ == "__main__":
# 创建子线程分别执行两个函数
t1 = Thread(target=sing)
t2 = Thread(target=dance)
# 开启子线程
t2.start()
t1.start()
2 > 参数:args,kwargs,方法:join()
# 多任务演示载歌载舞
import time
from threading import Thread
def sing(name:str,**kwargs):
"""唱歌"""
print(f"我来唱首{name}")
time.sleep(0.2) # 添加时间为了演示,否则程序运行时间太短
print("两只老虎,两只老虎跑得快")
time.sleep(0.2)
print("一直没有尾巴")
time.sleep(0.2)
print("一直没有耳朵")
time.sleep(0.2)
print("真奇怪,真奇怪!")
print('===================')
print(f"sing中还有一个字典{kwargs}")
print("唱歌结束。。。。。。。。。。。。。。")
def dance(name:str,**kwargs):
"""跳舞"""
print(f"我来跳段{name}")
print("跳舞动作:摇头")
time.sleep(0.2)
print("跳舞动作:挥手")
time.sleep(0.2)
print("跳舞动作:扭腰")
time.sleep(0.2)
print("跳舞动作:动脚")
print('===================')
print(f"dance中还有一个字典{kwargs}")
print("跳舞结束。。。。。。。。。。。。。。")
if __name__ == "__main__":
# 创建子线程分别执行两个函数
t1 = Thread(target=sing,args=('两只老虎',),kwargs={"info":"sing"})
t2 = Thread(target=dance,args=('disco',),kwargs={'info':'dance'})
# 开启子线程
t2.start()
t1.start()
# 主线程在此处等待子线程结束
# t1.join()
t2.join()
print("子线程都结束了")
3> setDaemon()
# 多任务演示载歌载舞
import time
from threading import Thread
def sing():
"""唱歌"""
print("我来为唱歌")
time.sleep(0.2) # 添加时间为了演示,否则程序运行时间太短
print("两只老虎,两只老虎跑得快")
time.sleep(0.2)
print("一直没有尾巴")
time.sleep(0.2)
print("一直没有耳朵")
time.sleep(0.2)
print("真奇怪,真奇怪!")
def dance():
"""跳舞"""
print("跳舞动作:摇头")
time.sleep(0.2)
print("跳舞动作:挥手")
time.sleep(0.2)
print("跳舞动作:扭腰")
time.sleep(0.2)
print("跳舞动作:动脚")
if __name__ == "__main__":
# 创建子线程分别执行两个函数
t1 = Thread(target=sing)
t2 = Thread(target=dance)
# 设置守护线程一定要在开启线程之前
t1.setDaemon(True)
# 开启子线程
t2.start()
t1.start()
t1.join() # 添加join主线程会等待t1结束,如果没有join则t1会随着主线程结束而结束
print("子线程都结束了") # 此行代码执行完毕不代表主线程执行结束,主线程会等待所有非守护子线程结束后才结束。
其他方法同学们自行练习
3 threading内置模块提供的方法
-
currentThread()/current_thread(): 返回当前的线程对象。
-
enumerate(): 返回一个包含正在运行的线程的列表。正在运行指线程启动后、结束前,不包括启动前和终止后的线程
-
activeCount()/active_count(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
# 多任务演示载歌载舞
import time
import threading
from threading import Thread
def sing():
"""唱歌"""
print("我来为唱歌")
time.sleep(0.2) # 添加时间为了演示,否则程序运行时间太短
print("两只老虎,两只老虎跑得快")
time.sleep(0.2)
print("一直没有尾巴")
time.sleep(0.2)
print("一直没有耳朵")
time.sleep(0.2)
print("真奇怪,真奇怪!")
# 查看当前线程
# print(f"在sing中线程:{threading.current_thread().getName()}") # 当前唱歌子线程
def dance():
"""跳舞"""
print("跳舞动作:摇头")
time.sleep(0.2)
print("跳舞动作:挥手")
time.sleep(0.2)
print("跳舞动作:扭腰")
time.sleep(0.2)
print("跳舞动作:动脚")
# 查看当前线程
# print(f"在dance中线程:{threading.current_thread().getName()}") # 当前跳舞子线程
if __name__ == "__main__":
# 创建子线程分别执行两个函数
t1 = Thread(target=sing, name='sing')
t2 = Thread(target=dance, name='dance')
# 设置守护线程一定要在开启线程之前
t1.setDaemon(True)
# 开启子线程
t2.start()
t1.start()
# 查看线程数量和当前线程
print(f"第一次查看线程数量:{threading.active_count()}")
print(f"第一次线程有:{threading.enumerate()}")
t1.join()
# 查看当前线程
# print(f"在主线程中:{threading.current_thread().getName()}")
# 查看线程数量和当前线程
print(f"第二次查看线程数量:{threading.active_count()}")
print(f"第二次线程有:{threading.enumerate()}")
2 使用继承方式开启线程
条件:
-
1> 自定义类,继承自threading中的Thread类
-
2>重写Thread中的run方法
import threading
class MyThread(threading.Thread):
def __init__(self,num):
super().__init__()
self.num = num
def run(self):
for i in range(self.num):
print(f"run:{i}")
if __name__ =="__main__":
my_thread = MyThread(3) # 创建线程
my_thread.start() # 开启线程
3 线程之间共享全局数据
进程是资源拥有者,而线程位于进程内部,所以同一进程内部的线程共享全局资源。
import threading
# 定义全局变量
g_num = 0
def add_num():
"""修改全局变量"""
global g_num
g_num += 1
def see_num():
"""查看g_num值"""
print(f"g_num值为{g_num}")
if __name__ == "__main__":
t1 = threading.Thread(target=add_num)
t2 = threading.Thread(target=see_num)
t1.start()
t2.start()
4 共享数据带来问题
修改上面的例子,现在两个线程函数都对同一个函数进行增加很多次,观察最终结果
import threading
# 定义全局变量
g_num = 0
def add_num1():
"""修改全局变量"""
global g_num
for _ in range(1000000):
g_num += 1
def add_num2():
"""修改g_num值"""
global g_num
for _ in range(1000000):
g_num += 1
if __name__ == "__main__":
t1 = threading.Thread(target=add_num1)
t2 = threading.Thread(target=add_num2)
t1.start()
t2.start()
# 保证子线程执行结束
t1.join()
t2.join()
print(f"g_num的值为{g_num}") # 发现每次值均不同,且比2000000小
为什么呢?这就说起我们说到的时间片轮转法,在一个线程执行的时间超过个一个时间片,那么不论线程是否执行完毕,那么CPU均切换到其他线程,而上一个线程可能完成了加法操作,但是没有将新的值赋值给全局变量,那么其他线程获得的仍旧是加之前的值。最终导致出现同一值可能出现连续被两个线程加,但是值只增加1.
这就是python中的线程不安全问题。
四 线程问题的解决
线程出现问题的原因本质就是由于两个线程同时对一个全局变量操作,但是这两个线程之间没有任何关系。那么如果我们让两个线程之间不再“自顾自”的运行,那么就可以解决。首先认识两个概念
1 同步和异步概念
同步:协同步调,按预定的先后次序执行。
理解:协同,不是指同时,如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A;A再继续操作
异步:与同步正好相反,两个任务之间没有任何关系,一个任务的执行完全不依赖于另一个任务。
那么线程问题原因就是由于两个线程是异步状态。
2 使用互斥锁解决
通过加锁,让两个线程之间产生依赖,解决线程不安全问题。即两个线程中获得到全局变量的线程立马加上一把锁,锁能够保证在解开锁之前,其他线程不能获得全局变量,只有线程一个操作流程结束,主动释放锁,其他线程才能再去获取全局变量。
咱们遇到的问题,python官方早已给出了方案,使用threading模块中的Lock即可实现锁。
# 创建锁
mutex = threading.Lock()
# 加锁
mutex.acquire(timeout) # 获得锁资源,会返回True
# 释放锁
mutex.release()
修改之前的代码
import threading
# 定义全局变量
g_num = 0
def add_num1():
"""修改全局变量"""
global g_num
for _ in range(1000000):
# 加锁
if mutex.acquire():
g_num += 1
# 释放锁
mutex.release()
def add_num2():
"""修改g_num值"""
global g_num
for _ in range(1000000):
# 加锁
if mutex.acquire():
g_num += 1
# 释放锁
mutex.release()
if __name__ == "__main__":
t1 = threading.Thread(target=add_num1)
t2 = threading.Thread(target=add_num2)
mutex = threading.Lock() # 创建线程锁
t1.start()
t2.start()
# 保证子线程执行结束
t1.join()
t2.join()
print(f"g_num的值为{g_num}")
但是,大家也发现,运行时间相比之前更长了,这就是上锁的代价。
3 总结:
锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行,不会出现数据安全问题
锁的坏处:
-
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
-
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
五 死锁
在多个线程共享资源的时候,如果两个线程分别占有一部分资源,并且同时等待对方的资源,就会造成死锁现象。如果锁之间相互嵌套,就有可能出现死锁。因此尽量不要出现锁之间的嵌套。
import threading
import time
def test1():
# 锁A上锁
if lockA.acquire():
print("test1的锁A....")
time.sleep(0.4)
# 锁B上锁
if lockB.acquire():
print("test1的锁B....")
# 释放锁B
lockB.release()
print("test1的释放锁B....")
else:
print("test1没有获得锁B")
# 释放锁A
lockA.release()
print("test1的释放锁A....")
else:
print("test1没有获得锁A")
def test2():
# 锁B上锁
if lockB.acquire():
print("test2的锁B....")
# 锁A上锁
if lockA.acquire():
print("test2的锁A....")
# 释放锁A
lockA.release()
print("test2的释放锁A....")
else:
print("test2没有获得锁A")
# 释放锁B
lockB.release()
print("test2的释放锁B....")
else:
print("test2没有获得锁B")
# 创建锁对象
lockA = threading.Lock()
lockB = threading.Lock()
if __name__ == "__main__":
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
死锁解决——添加超时时间
import threading
import time
def test1():
# 锁A上锁
if lockA.acquire():
print("test1的锁A....")
time.sleep(0.4)
# 锁B上锁
if lockB.acquire(timeout=2): # 此处添加超时时间即可
print("test1的锁B....")
# 释放锁B
lockB.release()
print("test1的释放锁B....")
else:
print("test1没有获得锁B")
# 释放锁A
lockA.release()
print("test1的释放锁A....")
else:
print("test1没有获得锁A")
def test2():
# 锁B上锁
if lockB.acquire():
print("test2的锁B....")
# 锁A上锁
if lockA.acquire():
print("test2的锁A....")
# 释放锁A
lockA.release()
print("test2的释放锁A....")
else:
print("test2没有获得锁A")
# 释放锁B
lockB.release()
print("test2的释放锁B....")
else:
print("test2没有获得锁B")
# 创建锁对象
lockA = threading.Lock()
lockB = threading.Lock()
if __name__ == "__main__":
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
扩展:银行家算法
六 局部变量不共享
1 使用继承方式
import threading
import time
class MyThread(threading.Thread):
def __init__(self,num):
super().__init__()
self.num = num
def run(self):
self.num += 1
# time.sleep(1)
print(self.name,self.num)
t1 = MyThread(100)
t1.start()
t2 = MyThread(200)
t2.start()
2 直接使用Thread类
import threading
# 定义全局变量
g_num = 0
def add_num1():
"""修改全局变量"""
global g_num
num1 = 100
num1 += 1
g_num += 1
print(f"在num1中全局变量:{g_num}")
print(f"在num1中局部变量:{num1}")
def add_num2():
"""修改全局变量"""
global g_num
num2 = 200
num2 += 1
g_num += 1
print(f"在num2中全局变量:{g_num}")
print(f"在num2中局部变量:{num2}")
if __name__ == "__main__":
t1 = threading.Thread(target=add_num1)
t2 = threading.Thread(target=add_num2)
t1.start()
t2.start()
# 保证子线程执行结束
t1.join()
t2.join()
print(f"g_num的值为{g_num}")
七 生产者消费者模式
(一)引入
在并发编程中,如果两个任务之间有一定关系,我们把这两个任务比作生产者和消费者,那么消费者要买商品的话,必须要求商家已经将商品生产出来。理想状态下,生产者生产一件商品,消费者恰好需要。但是如果产能不够,那么会造成消费者迟迟买不到东西,反之,如果产能过剩,会造成库存堆积。那么为了解决二者之间的矛盾,我们需要引入一个库存。消费者尽管去库存中获取商品,生产者尽管生产商品。这样生产者和消费者之间不再直接关联,从而达到解耦作用。
那么使用什么当做库存呢?队列
(二) 队列
1 队列对象创建
python内置了队列模块,我们可以直接导入使用import queue
。根据不同的需求,python内置的模块提供了四种队列:
- 先进先出队列:Queue(FIFO):
q = queue.Queue(maxsize=0)
- 后进先出队列:LifoQueue(LIFO):
q = queue.LifoQueue(maxsize=0)
- 优先级队列:PriorityQueue:
q = queue.PriorityQueue(maxsize=0)
- 简单队列:SimpleQueue(了解):简单的Queue队列。3.7新增
2 队列对象常用操作方法(来源官方文档)
队列对象 (Queue
, LifoQueue
, 或者 PriorityQueue
) 提供下列描述的公共方法。
-
Queue.qsize
()返回队列的大致大小。注意,qsize() > 0 不保证后续的 get() 不被阻塞,qsize() < maxsize 也不保证 put() 不被阻塞。
-
Queue.empty
()如果队列为空,返回
True
,否则返回False
。如果 empty() 返回True
,不保证后续调用的 put() 不被阻塞。类似的,如果 empty() 返回False
,也不保证后续调用的 get() 不被阻塞。 -
Queue.full
()如果队列是满的返回
True
,否则返回False
。如果 full() 返回True
不保证后续调用的 get() 不被阻塞。类似的,如果 full() 返回False
也不保证后续调用的 put() 不被阻塞。 -
Queue.put
(item, block=True, timeout=None)将 item 放入队列。如果可选参数 block 是 true 并且 timeout 是
None
(默认),则在必要时阻塞至有空闲插槽可用。如果 timeout 是个正数,将最多阻塞 timeout 秒,如果在这段时间没有可用的空闲插槽,将引发Full
异常。反之 (block 是 false),如果空闲插槽立即可用,则把 item 放入队列,否则引发Full
异常 ( 在这种情况下,timeout 将被忽略)。 -
Queue.put_nowait
(item)相当于
put(item, False)
。 -
Queue.get
(block=True, timeout=None)从队列中移除并返回一个项目。如果可选参数 block 是 true 并且 timeout 是
None
(默认值),则在必要时阻塞至项目可得到。如果 timeout 是个正数,将最多阻塞 timeout 秒,如果在这段时间内项目不能得到,将引发Empty
异常。反之 (block 是 false) , 如果一个项目立即可得到,则返回一个项目,否则引发Empty
异常 (这种情况下,timeout 将被忽略)。POSIX系统3.0之前,以及所有版本的Windows系统中,如果 block 是 true 并且 timeout 是None
, 这个操作将进入基础锁的不间断等待。这意味着,没有异常能发生,尤其是 SIGINT 将不会触发KeyboardInterrupt
异常。 -
Queue.get_nowait
()相当于
get(False)
。
提供了两个方法,用于支持跟踪 排队的任务 是否 被守护的消费者线程 完整的处理。
-
Queue.task_done
()(了解)表示前面排队的任务已经被完成。被队列的消费者线程使用。每个
get()
被用于获取一个任务, 后续调用task_done()
告诉队列,该任务的处理已经完成。如果join()
当前正在阻塞,在所有条目都被处理后,将解除阻塞(意味着每个put()
进队列的条目的task_done()
都被收到)。如果被调用的次数多于放入队列中的项目数量,将引发ValueError
异常 。 -
Queue.join
()(了解)阻塞至队列中所有的元素都被接收和处理完毕。当条目添加到队列的时候,未完成任务的计数就会增加。每当消费者线程调用
task_done()
表示这个条目已经被回收,该条目所有工作已经完成,未完成计数就会减少。当未完成计数降到零的时候,join()
阻塞被解除。
import queue,threading
# 创建三种队列
q1 = queue.Queue()
q2 = queue.LifoQueue()
q3 = queue.PriorityQueue()
# qsize()
# print(q1.qsize())
# print(q2.qsize())
# print(q3.qsize())
# empty()
# print(q1.empty())
# print(q2.empty())
# print(q3.empty())
# full()
# print(q1.full())
# print(q2.full())
# print(q3.full())
# put(item)
# 先进后出
# print(q2.empty())
# q2.put('num1')
# q2.put('num2')
# q2.put('num3')
# print(q2.empty())
# print(q2.get())
# print(q2.get())
# print(q2.get())
# 先进先出队列
# print(q1.empty())
# q1.put('num1')
# q1.put('num2')
# q1.put('num3')
# print(q1.empty())
# print(q1.get())
# print(q1.get())
# print(q1.get())
# 优先级队列:需要我们指定优先级,若果未指定则是先进先出
print(q3.empty())
q3.put('num1')
q3.put('num2')
q3.put('num3')
print(q3.empty())
print(q3.get())
print(q3.get())
print(q3.get())
# 指定优先级
print(q3.empty())
q3.put((2,'num1'))
q3.put((1,'num2'))
q3.put((2,'num3'))
print(q3.empty())
print(q3.get())
print(q3.get())
print(q3.get())
# 官方实例
import threading, queue
q = queue.Queue()
def worker():
while True:
item = q.get()
print(f'Working on {item}')
print(f'Finished {item}')
q.task_done()
# turn-on the worker thread
threading.Thread(target=worker, daemon=True).start()
# send thirty task requests to the worker
for item in range(30):
q.put(item)
print('All task requests sent\n', end='')
# block until all tasks are done
q.join()
print('All work completed')
(三)生产消费者模式
使用队列将生产者和消费者解耦
实现读写分离操作
import queue
import threading
import time
def write(q,f):
while True:
num = q.get()
f.write(num)
print(f'成功将数字{num}写入到文件')
time.sleep(0.6)
q.task_done()
def read(q):
for i in range(10):
q.put(f'数字{i}')
print(f'读取到数字{i}')
time.sleep(0.2)
if __name__ == '__main__':
q = queue.Queue()
f = open('p-c.txt', 'w', encoding='utf8')
t1 = threading.Thread(target=write, args=(q, f,),daemon=True)
t2 = threading.Thread(target=read, args=(q,))
t1.start()
t2.start()
q.join() #注意要先结束后关闭文件
f.close()
八 local对象
在操作线程时候,如果多个线程使用同一个全局变量那么容易出现数据安全问题,如果每个线程自己独立操作自己内部的数据,即局部变量,那么其他线程又不能使用。那么按照普通方式,我们可以使用函数传参
1 使用参数传参
当传递层次较多,传递不方便
class Student:
def __init__(self,name,age):
self.name = name
self.age = age
def process_stu():
stu = Student('zs',20)
do_task1(stu)
do_task2(stu)
def do_subtask1(stu):
pass
def do_subtask2(stu):
pass
def do_task1(stu):
print(stu.name)
do_subtask1(stu)
def do_task2(stu):
print(stu.age)
do_subtask2(stu)
2 使用全局字典
定义全局字典,字典的键使用线程对象,那么每个线程获取各自信息
可以实现,但是不是最简单
import threading
global_dict = {}
class Student:
def __init__(self,name,age):
self.name = name
self.age = age
#
def process_stu():
stu = Student('zs',20)
global_dict[threading.current_thread()] = stu
do_task1()
do_task2()
def do_subtask1(stu):
pass
def do_subtask2(stu):
pass
def do_task1():
stu = global_dict[threading.current_thread()]
print(stu.name)
do_subtask1(stu)
def do_task2():
stu = global_dict[threading.current_thread()]
print(stu.age)
do_subtask2(stu)
3 使用local对象
通过实例化local类,获得local对象,这样保证线程内部的数据在各自线程内部可以共享,但是在不同线程不能互相使用。其实内部local对象也是维护一个字典,用当前线程作为字典的键
import threading
stus = threading.local()
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def process_stu(name, age):
stu = Student(name, age)
stus.stu = stu
do_task1()
do_task2()
def do_subtask1():
stus.stu.name = 'new_' + stus.stu.name
print(f"修改后姓名:{stus.stu.name}")
def do_subtask2():
stus.stu.age = 5 + stus.stu.age
print(f"修改后年龄{stus.stu.age}")
def do_task1():
print(stus.stu.name)
do_subtask1()
def do_task2():
print(stus.stu.age)
do_subtask2()
if __name__ == '__main__':
threading.Thread(target=process_stu, args=('zs', 20)).start()
threading.Thread(target=process_stu, args=('ls', 40)).start()
九全局解释器锁(GIL)
1 GIL不是Python特性
GIL是Python解释器(Cpython)引入的概念,在JPython、PyPy中没有GIL,因为CPython是大部分环境下默认的Python执行环境,所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷,所以GIL并不是python的特性,仅仅是因为历史原因在Cpython解释器中难以移除。
2 作用机制
Python的线程虽然是真正的线程,但解释器执行代码时,任何Python线程执行前,必须先获得GIL锁,然后执行时间片操作(龟叔开发python的时候只有单核,为了让各个线程能够平均利用CPU时间),解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行。
3 GIL存在原因或作用
Guido van Rossum(吉多·范罗苏姆)创建python时考虑到单核cpu,CPython在执行多线程的时候并不是线程安全的,所以为了程序的稳定性解决多线程之间数据完整性和状态同步的最简单方法就是加锁, 于是有了GIL这把超级大锁。它能够确保任何时候都只有一个Python线程在执行。
4 GIL的弊端
GIL对计算密集型的程序会产生影响。因为计算密集型的程序,需要占用系统资源。GIL的存在,相当于始终在进行单线程运算,这样自然就慢了。如果多任务很多的话,除了GIL,系统也会把大量时间用在切换上,效率也是急剧下降的,甚至运行效率低于单线程执行。
5 GIL好处
简单来说,它在单线程的情况更快,并且在和 C 库结合时更方便,而且不用考虑线程安全问题,尤其在i/o密集型场景下,GIL在遇到文件i/o会自动释放锁。
示例:单线程和双线程处理同样数据
from threading import Thread
import time
def func1():
s_time = time.time()
i = 0
for _ in range(200000000):
i += 1
e_time = time.time()
print(f"耗费:{e_time-s_time}")
print(i)
def func2():
s_time = time.time()
i = 0
for _ in range(100000000):
i += 1
e_time = time.time()
print(f"耗费时间:{e_time-s_time}")
print(i)
if __name__ == '__main__':
# func1() # 耗费:14.419824600219727
for _ in range(2):
Thread(target=func2).start() # 耗费时间:14.452826738357544
使用多进程测试:有改进,但是并不是缩短一半时间,因为切换任务也需要时间
# from threading import Thread
# import time
#
# def func1():
# s_time = time.time()
# i = 0
# for _ in range(200000000):
# i += 1
# e_time = time.time()
# print(f"耗费:{e_time-s_time}")
# print(i)
#
#
# def func2():
# s_time = time.time()
# i = 0
# for _ in range(100000000):
# i += 1
# e_time = time.time()
# print(f"耗费时间:{e_time-s_time}")
# print(i)
#
# if __name__ == '__main__':
# # func1() # 耗费:14.419824600219727
# for _ in range(2):
# Thread(target=func2).start() # 耗费时间:14.452826738357544
#
# 多进程
from multiprocessing import Process
import time
def func1():
s_time = time.time()
i = 0
for _ in range(200000000):
i += 1
e_time = time.time()
print(f"耗费:{e_time-s_time}")
print(i)
def func2():
s_time = time.time()
i = 0
for _ in range(100000000):
i += 1
e_time = time.time()
print(f"耗费时间:{e_time-s_time}")
print(i)
if __name__ == '__main__':
# func1() # 耗费:14.955855369567871
for _ in range(2):
Process(target=func2).start() # 耗费时间:10.602606296539307
练习:火车站抢票系统
import queue
import threading
import time
def buy(q,name):
while True:
num = q.get()
print(f'{name}成功抢到第{num}张票')
time.sleep(0.3)
q.task_done()
def sale(q):
for i in range(1,101):
print(f'即将开始售卖第{i}张票')
time.sleep(0.5)
q.put(i)
if __name__ == '__main__':
q = queue.Queue()
f = open('p-c.txt', 'w', encoding='utf8')
c1 = threading.Thread(target=buy, args=(q, "zs"),daemon=True)
c2 = threading.Thread(target=buy, args=(q, "ls"),daemon=True)
c3 = threading.Thread(target=buy, args=(q, "ww"),daemon=True)
p = threading.Thread(target=sale, args=(q,))
c1.start()
c2.start()
c3.start()
p.start()
q.join() #注意要先结束后关闭文件
f.close()