GIL详解
GIL锁 *******
全名是(Global Interpreter Lock 全局解释器锁)
官方解释:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from eecuting 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中,GIL会把线程的并行编程串行,导致效率降低。
但需要知道的是,解释器并不只有CPython,还有PyPy,JPython等等。GIL也仅存在与CPython中
这并不是Python这门语言的问题,而是CPython这个解释器的问题。
非线程安全: 即多个线程访问同一个资源,会有问题
线程安全:即多个线程访问同一个资源,不会有问题
CPython中有一个互斥锁,防止线程同一时间执行python代码
该锁只存在Cpython中。
之所以用CPython的原因:1.C编译过的结果可以让计算机直接识别
2.C语言中大量现成的库(算法),CPython可以无缝链接c语言的任何现成代码。
python是胶水语言,就是这也能干,那也能干。
内存管理:
垃圾回收机制
python中不需要手动管理内存,它有个回收垃圾线程GC,也是python自带的内存管理机制。
GIL与GC的孽缘:
要先搞清楚这个问题,需先了解GC的工作原理,python中内存管理使用的是引用计数,每个数会被加上一个整型的计数器,表示这个数据被引用的次数,当这个整数变为0时则表示这个数据已经没有人使用,成了垃圾数据。
当内存占用达到某个阈值时,GC会将其他线程挂起,然后执行垃圾清理操作,垃圾清理也是一串代码,也就需要一条线程来执行。
示例:
from threading import Thread def task(): a = 10 print(a) # 开启三个子线程执行task函数 Thread(target=task).start() Thread(target=task).start() Thread(target=task).start()
通过上图可以看出,GC与其他线程都在竞争解释器的执行权,而CPU何时切换,以及
切换到哪个线程都是无法预知的,这样一来就造成了竞争问题,假设线程1正在定义变量a = 10,而定义变量第一步会先到内存中申请空间把10存进去,第二步将10的内存地址与变量名a进行绑定,如果在执行完第一步后,cpu切换到了GC线程,GC线程发现10的地址引用计数为0则将其当成垃圾进行了清理,等cpu再次切换到线程1时,刚刚保存的数据10已经被清理掉了,导致无法正常定义。
当然其他一些涉及到内存的操作同样可能产生问题,为了避免GC与其他线程竞争解释器带来的问题,CPython简单粗暴的给解释器加了互斥锁。
有了GIL后,多个线程将不可能在同一时间使用解释器,从而保证了解释器的数据安全。
GIL锁的加锁和解锁时机:
加锁的时机:在调用解释器时立即加锁
解锁时机:1.当前线程遇到了IO时释放。2.当前线程执行时间超过设定值时释放。
关于GIL的性能讨论:
GIL的优点:
保证了CPython中的内存管理是线程安全的
GIL的缺点:
互斥锁的特性使得多线程无法并行
-
GIL仅仅在CPython解释器中存在,在其他的解释器中没有,并不是Python这门语言的缺点
-
在单核处理器下,多线程之间本来就无法真正的并行执行
-
在多核处理下,运算效率的确是比单核处理器高,但是要知道现代应用程序多数都是基于网络的(qq,微信,爬虫,浏览器等等),CPU的运行效率是无法决定网络速度的,而网络的速度是远远比不上处理器的运算速度,则意味着每次处理器在执行运算前都需要等待网络IO,这样一来多核优势也就没有那么明显了
举个例子:
任务1 从网络上下载一个网页,等待网络IO的时间为1分钟,解析网页数据花费,1秒钟
任务2 将用户输入数据并将其转换为大写,等待用户输入时间为1分钟,转换为大写花费,1秒钟
单核CPU下:1.开启第一个任务后进入等待。2.切换到第二个任务也进入了等待。一分钟后解析网页数据花费1秒解析完成切换到第二个任务,转换为大写花费1秒,那么总耗时为:1分+1秒+1秒 = 1分钟2秒
多核CPU下:1.CPU1处理第一个任务等待1分钟,解析花费1秒钟。1.CPU2处理第二个任务等待1分钟,转换大写花费1秒钟。由于两个任务是并行执行的所以总的执行时间为1分钟+1秒钟 = 1分钟1秒
可以发现,多核CPU对于总的执行时间提升只有1秒,但是这边的1秒实际上是夸张了,转换大写操作不可能需要1秒,时间非常短!
上面的两个任务都是需要大量IO时间的,这样的任务称之为IO密集型,与之对应的是计算密集型即IO操作较少大部分都是计算任务。
对于计算密集型任务,Python多线程的确比不上其他语言!为了解决这个弊端,Python推出了多进程技术,可以良好的利用多核处理器来完成计算密集任务。
总结:
1.单核下无论是IO密集还是计算密集GIL都不会产生任何影响
2.多核下对于IO密集任务,GIL会有细微的影响,基本可以忽略
3.Cpython中IO密集任务应该采用多线程,计算密集型应该采用多进程
另外:之所以广泛采用CPython解释器,就是因为大量的应用程序都是IO密集型的,还有另一个很重要的原因是CPython可以无缝对接各种C语言实现的库,这对于一些数学计算相关的应用程序而言非常的happy,直接就能使用各种现成的算法
自定义的线程锁与GIL的区别:
GIL保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等,具体参考垃圾回收机制详解。
对于程序中自己定义的数据则没有任何的保护效果,这一点在没有介绍GIL前我们就已经知道了,
所以当程序中出现了共享自定义的数据时就要自己加锁
进程池与线程池:
池表示一个容器,本质上就是一个存储进程或线程的列表
池中存储线程还是存储进程?
如果是IO密集型就存储线程,计算密集型就存储进程
为什么需要进程/线程池?
在很多情况下需要控制进程或线程的数量在一个合理的范围,例如TCP程序中,一个客户端对应
一个线程,虽然线程的开销小,但肯定不能无限的开,否则系统资源迟早被耗尽,解决的办法就是控制线程的数量。
线程/进程池不仅帮我们控制线程/进程的数量,还帮我们完成了线程/进程的创建,销毁,以及任务的分配。
同步异步-阻塞非阻塞:
阻塞与非阻塞说的是程序的运行的状态
并发与并行说的是程序处理的方式
同步与异步说的是任务的提交或执行方式
同步:发起任务后,进入等待状态,必须等到任务执行完成后才能继续执行
异步:发起任务后,不需要等待,可以执行其他的操作。
同步会有等待的效果但是这和阻塞是完全不同的,阻塞时程序会被剥夺cpu执行权,而同步调用则不会!
异步回调:
什么是异步回调:
异步回调指的是:在发起一个异步任务的同时指定一个函数,在异步任务完成时会自动的调用这个函数。
为什么需要异步回调?:
之前在使用线程池或进程池提交任务时,如果想要处理任务的执行结果则必须调用result函数或是shutdown函数,而它们都是阻塞的,会等到任务执行完毕后才能继续执行,这样一来在这个等待过程中就无法执行其他任务,降低了效率,所以需要一种方案,即保证解析结果的线程不用等待,又能保证数据能够及时被解析,该方案就是异步回调