什么是GIL?
GIL全称是Global Interpreter Lock,是python最常见的解释器CPython引入的一个概念。GIL是为了避免多个线程(threads)同时执行。因为CPython的内存管理并不是线程安全的,所以这个锁的存在是有必要的,短时间也是无法移除的。
GIL是一把全局排他锁。毫无疑问,全局锁的存在会多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序。
影响
GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势。
GIL只会影响到那些严重依赖CPU的程序(计算型的),如果你的程序大部分只会涉及到I/O,比如网络交互,那么使用多线程就很合适。因为他们大部分时间都是在等待(线程被限制到同一时刻只允许一个线程执行这样一个执行模型。GIL会根据执行的字节码行数和时间片来释放GIL,在遇到IO操作的时候会主动释放权限给其他线程)。
所以,Python的线程更适用于处理I/O和其他需要并发执行的堵塞操作,而不是需要多处理器并行的计算密集型任务。对于IO操作来说,多进程和多线程性能差别不大。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少。
我们可以认为:
- Python中同一时刻有且只有一个线程会执行
- Python中的多个线程由于GIL锁的存在无法利用多核CPU
- Python中的多线程不适合计算密集型的程序
GIL的用处:
GIL对诸如当前线程状态和为垃圾回收二用的堆分配对象这样的访问提供着保护。
如何避免影响
由于是解释器层面的锁,无法去除GIL锁在执行程序时带来的问题。只能去改善。
用multiprocess替代Thread
即使用多进程而不是多线程。每个进程有自己的独立的GIL,因此不会出现进程之间的GIL争抢。
缺点:线程之间共享变量比较容易,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocess由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。
使用其他解释器
JPython和IronPython没有GIL的问题,但是比较小众。功能和性能较差,不是个好选择。
核心部分使用其他语言
如果对并行计算性能较高的程序可以考虑把核心部分写成C模块,或者索性用其他语言实现
总结,GIL在较长一段时间内将会继续存在,但是会不断对其进行改进。
进程
Python中的进程基本是对操作系统原生进程的包装,比如Linux的fork。在Python标准库中,主要是multiprocessing包,多么直白的名字。其中常用的也就是pool,queue模块以及synchronize模块重的一些同步原语(Lock、Condition、Semaphore、Event等)。
线程
与进程一样,Python中的线程也是对系统原生线程的包装。其实现在的Linux上,线程和进程的差别不是很大,以此推知,Linux平台下,Python中的线程和进程开销差别也不会太大,但终归进程是要开销大点的,创建也会慢一点。相比于进程,我是更倾向使用线程的,尤其是IO密集型程序,能用线程解决的问题,尽量不用进程。
协程
前面我们提到了,Python的线程(包括进程)其实都是对系统原生内核级线程的包装,切换时,需要在内核与用户态空间来来回回,开销明显会大很多,而且多个线程完全由系统来调度,什么时候执行哪个线程是无法预知的。相比而言,协程就轻量级很多,是对线程的一种模拟,原理与内核级线程类似,只不过切换时,上下文环境保存在用户态的堆栈里,协程“挂起”的时候入栈,“唤醒”的时候出栈,所以其调度是可以人为控制的,这也是“协程”名字的来由,大伙协作着来,别总抢来抢去的,伤感情。
实际上,协程本身并不是真正的并发,任何时候只有一个协程在执行,只是当需要耗时操作时,比如I/O,我们就让它挂起,执行别的协程,而不是一直干等着什么也做不了,I/O完毕了我们再切换来继续执行,这样可以大大提高效率,而且不用再费心费力去考虑同步问题,简单高效。与传统线程、进程模型相比,协程配上事件循环(告诉协程什么时候挂起,什么时候唤醒),简直完美。Python里的协程也是后来才逐渐加入的,基本分三个阶段,过程比较坎坷,与”携程“差不多,时不时被骂几句。