复习
1.JoinableQueue--可以被join的队列
2.多线程
3线程的使用方法与进程一模一样
3.1守护线程
3.2线程安全问题
3.3解决方案
3.3.1互斥锁mutex
3.3.2递归锁Rlock
3.3.3信号量semaphore
3.3.4死锁问题
详解:
1.JoinableQueue--可以被join的队列
1.1join是等待任务结束
队列怎么叫结束
调用task_done一次则表示有一个数据被处理完成了,当task_done次数等于put的次数就意味着任务处理完成了
1.2这就是join的执行时机
该队列已经明确告知数剧的使用方,所有数据都已经处理完成
1.3在生产者消费者模型中解决了消费者,不知道何时算是任务结束的问题
具体过程:主进程先等待所有的生产者进程生成完毕,再等队列中的数据被全部处理,这就意味着,任务全部结束
2.多线程
2.1使用多线程|多进程目的:
为了 并发执行任务,从而提高效率
2.2什么是线程?
线程是操作系统运算调度的最小单位(CPU最小执行单位),线程被包含在进程中,一个线程就是一个固定的执行流程(控制流)
2.3线程的特点:
进程是不能被执行的,进程是一个资源单位,其中 包含了程序运行所需的所有资源
线程:才是真正的执行单位,光有进程和程序是无法运行的,必须先创建进程将资源加载到进程中,再启动线程来执行任务
一个进程至少包含一个线程,称之为主线程,主线程是由操作系统来开启的
一个进程可以包含多个线程来提高程序的效率
2.4线程与进程的区别
1.线程创建的开销远小于进程
2.统一进程中的所有线程共享进程内的资源
3.线程之间没有父子关系,是平等的,PID相同
2.5如何选择进程与线程?
要根据具体的任务类型,IO密集(线程) 计算密集(进程)
3.线程的使用方法与进程一模一样
区别:开启线程的位置可以是任意位置
3.1守护线程
守护线程会在所有非守护线程结束时一起结束,当然守护可以提前结束(任务完成的情况下)
3.2线程安全问题
问题:并发操作同一个资源,可能导致数据错乱
解决方案:加互斥锁
3.3解决方案
1.互斥锁mutex--lock
2.递归锁Rlock
同一线程可以多次执行acquire()
3.信号量semaphore--仅用于控制并发访问
4.死锁问题:
不止一个锁,分别被不同的线程持有,相互等待对方释放,就会导致锁死问题
如何避免死锁问题?
锁不要有多个,一个足够
如果真的发生了死锁问题,必须迫使一方先交出锁
今日内容
1.GIL--全局解释器锁
2.GIL带来的问题
3.为什么需要GIL
4.GIL的加锁解锁时机
5.关于GIL的性能的讨论
6.线程常用方法
7.GIL锁与自定义锁的区别
8.进程池与线程池
9.同步异步
10.异步调用
详解:
1.GIL--全局解释器锁
官方解释:
'''
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)
'''
释义:
在CPython 中,这个全局解释器锁,也称之为GIL,是一个互斥锁,防止多个线程在同一时间执行Python字节码,这个锁是非常重要的,因为CPython的内存管理非线程安全的,很多其他的特性依赖于GIL,所以即使他影响了程序效率也无法将其直接去除
非线程安全:即多个线程访问同一个资源,会有问题
线程安全:即多个线程访问同一个资源,不会有问题
作用:CPython中有一个互斥锁,防止线程同一时间执行python代码
注意:
该锁只存在CPython中,这并不是Python这门语言的问题,而是解释器的问题,除了CPython,还有JPython,pypy解释器
总结:在CPython中,GIL会把线程的并行变成串行,导致效率降低
2.GIL带来的问题
GIL全局解释器锁 本质:是一把互斥锁
前提条件:
1.解释器的作用(py文件中的内容本质都是字符串,只有在被解释器解释时,才具备语法意义)
2.解释器与应用程序代码之间的关系(解释器会将py代码翻译为当前系统支持的指令交给系统执行。)
影响:
1.当进程中仅有一条线程时,GIL锁存在不会有任何影响
2.但是如果进程中有多个线程时,GIL锁就开始发挥作用:
1.开启子线程时,子线程指定一个target表示该子线程要处理的任务即将要执行的代码,
2.代码要执行必须交给解释器,即多个线程之间就要共享解释器,
3.为了避免共享带了的数据竞争问题,于是就给解释器加上了互斥锁。
总结:对应用程序造成什么影响?
由于互斥的特性,程序串行,保证了数据安全,降低了执行效率,GIL将使得程序整体效率降低
为什么Cpython要这么设计?
Cpython诞生于1991年 而多核处理器诞生2004年
当时不需要考虑多核效率问题
现在为什么不拿掉这个锁,因为这期间,很多已经完成的代码都依赖于这个锁,如果直接拿到,这些代码全得该,成本太大了
3.为什么需要GIL?
1.GC:内存管理机制
在使用python进行编程时,程序员无需参与内存管理,因为有Python自带的内存管理机制,简称GC
2.Python内存管理--使用引用计数 了解
垃圾回收机制:
Python中不需要手动管理内存,C,OC
引用计数:
每个数会被加上一个整型的计数器,表示这个数据被引用的次数
a=10 10地址次数计数为1
b=a 计数2
b=1 计数1
a=0 计数0 表示该数据已无人使用,变为垃圾数据
内存管理:
当内存占用达到阈值,GC停止其他线程,启动垃圾清理操作(一串代码。一个线程)
当垃圾回收启动后会将计数为0的数据清除掉,回收内存
分代回收:
自动垃圾回收其实就是说,内部会有一个垃圾回收线程,会在某一时间运行起来,开始清理垃圾
这可能会产生问题:
例如线程1申请了内存,但是还没有使用CPU切换到了GC,GC将数据当成垃圾清理掉了
解决问题:
为了解决问题,CPython就给解释器加了互斥锁,多个线程将不可能同一时间使用解释器,保证了数据的安全性
4.GIL的加锁解锁时机
加锁:
只有一个线程要使用解释器就立马枷锁(即:在调用解释器时立即加锁)
释放:
该线程任务结束
该线程遇到IO
该线程使用解释器过长(超过设定值) 默认100纳秒
5.关于GIL的性能的讨论
5.1GIL:
优点:保证了CPython中的内存管理是线程安全的
缺点:互斥锁的特性使得多线程无法并行,严重降低了运行效率
5.2但我们也不能就此给Python语言全部判负,其原因如下:
1. GIL仅仅在CPython解释器中存在,在其他的解释器中没有,并不是Python这门语言的缺点
2. 在单核处理器下,多线程之间本来就无法真正的并行执行
3. 在多核处理下,运算效率的确是比单核处理器高,但是要知道现代应用程序多数都是基于网络的(qq,微信,爬虫,浏览器等等),CPU的运行效率是无法决定网络速度的,而网络的速度是远远比不上处理器的运算速度,则意味着每次处理器在执行运算前都需要等待网络IO,这样一来多核优势也就没有那么明显了
5.3GIL给我们造成的影响
多线程不能并行
案例:
1.IO密集型---使用多线程
有一个下载任务 要从网络中下载一个文件 大小1G
和转换任务 使用input 转为大写输出
上述任务并行执行,耗时也不会有太大提升,反而开启多进程会浪费更多资源
这种任务称之为IO密集型,大量的时间都花在IO等待
2.计算机密集型任务(即IO操作较少大部分都是计算任务。)
图像处理,语音处理,大数据分析
总结:**
1.单核下无论是IO密集还是计算密集GIL都不会产生任何影响
2.多核下对于IO密集任务,GIL会有细微的影响,基本可以忽略
3.Cpython中IO密集任务应该采用多线程,计算密集型应该采用多进程
注意:
1.之所以广泛采用CPython解释器,就是因为大量的应用程序都是IO密集型的,
2.还有另一个很重要的原因是CPython可以无缝对接各种C语言实现的库
# 解决方案: `*****`
区分任务类型
1.如果是IO密集使用多线程
2.如果是计算密集使用多进程
6.线程常用方法
# 1.print(active_count())获取存活的线程数量
# 2.print(current_thread().getName())获取线程的名称
# 3.print(enumerate())获取正常运行的所有线程对象
# 4.print(current_thread())获取当前线程对象
7.GIL锁与自定义锁的区别
都是互斥锁
为什么有了GIL还需要自己加锁
GIL是加在解释器上的,只能锁住,解释器内部的资源,但是无法锁住我们自己开启资源,所以当程序中出现了共享自定义的数据时就要自己加锁
8.进程池与线程池******
池就是容器
本质上就是装线程|装进程的容器
池子中存储线程还是进程?
1.如果是IO密集型任务使用线程池, 2.如果是计算密集任务则使用进程池
优点:
1.自动管理线程的开启和销毁
2.自动分配任务给空闲的线程
3.可以限制开启线程的数量,保证系统的稳定
注:信号量中是限制同时并发多少,但是线程已经全都建完了
如何使用?
1.创建池子
pool = ProcessPoolExecutor()
pool=ThreadPoolExecutor
2.submit提交任务
pool.submit(task,"jerry")
3.pool.shutdown()#等待所有任务完毕,销毁所有线程,后关闭线程池
pool.shutdown()
注:关闭后就不能提交新任务了
9.同步 异步
9.1在并发中 经常提及的几个概念
阻塞--非阻塞 程序的状态
程序的运行状态 非阻塞 可能就绪 或者 运行
并发--并行 多任务处理方式
多个任务看起来像是同时运行 ,本质是切换+保存状态
并行真正的同时进行中, 必须具备多核处理器
同步--异步 任务提交执行的方式
同步==阻塞 × 卡住 == 阻塞 ×
异步 == 非阻塞 ×
同步:指的是发起任务后,必须在原地等待,直到任务完成拿到结果
默认情况就是同步
异步:发起任务,不需等待结果,可以继续执行其他代码,异步
异步必须依赖并发或者并行,在python中,通过多线程或多进程
异步的效率明显高于同步
如何使用?
#1. res = pool.submit(task).发起一个异步任务 # 异步调用 res就是一个表示异步任务的对象
# 2.定义一个回调函数 传的参数就是一个完成后任务对象
def finished(arg):
print(arg.result())
print("黑牛买回来了! ")
pass
# 3.res.add_done_callback(finished)给这个异步任务添加了一个回调函数
案例说明:
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time
from concurrent.futures._base import Future
pool = ThreadPoolExecutor() # my cpu_count = 6 so 6 * 5 = 30
def task():
time.sleep(2)
print("执行完成!")
return "一瓶黑牛!"
print("start")
# task()# 同步调用
# 1.发起一个异步任务
res = pool.submit(task) # 异步调用 res就是一个表示异步任务的对象
# 2.定义一个回调函数 传的参数就是一个完成后任务对象
def finished(arg):
print(arg.result())
print("黑牛买回来了! ")
pass
# 3.给这个异步任务添加了一个回调函数
res.add_done_callback(finished)
# pool.shutdown() # 阻塞直到线程池所有任务全部完成 会导致主线卡在原地
# print(res)
# print("执行的结果:",res.result()) # 会导致主线卡在原地
print("over")
10.异步回调
异步指的是任务的提交方式是异步的
10.1 异步任务的问题:
如果这个任务执行完成后会产生返回值,任务发起方该何时去获取结果
解决方案: 异步回调
10.2异步回调
指的就是一个函数,该函数会在任务后自动被调用,并且会传入Future对象 ,
通过Future对象的result()获取执行结果 ,
有了回调函数 就可以在任务完成时 及时处理它
10.3通常异步任务都会绑定一个回调函数,用来处理任务结果
1.在进程池中回调函数是在父进程中执行,原因是 任务是由父进程发起的,所以结果也应该交给父进程
2.在线程池中回调函数就在子线程中执行,原因是 线程之间数据本来是共享的
3.如果你的任务结果需要交给父进程来处理,那建议回调函数,回调函数会自动将数据返回给父进程,不需要自己处理IPC