业界的python程序员都会说python的线程是一个假的线程
python的线程,的的确确封装了底层的操作系统线程,在linux系统里是Pthread(全称为POSIX Thread),而在windows系统里是windows Thread。另外,python线程也完全受操作系统管理,比如协调何时执行,管理内存资源,管理中断等等。
所以,虽然python的线程和c++的线程本质上是不同的抽象,但它们的底层并没有不同。
为什么要有GIL
GIL是最流行的python解释器CPython中的一个技术术语。它的意思是全局解释器锁,本质上是类似操作系统的Mutex。每一个python线程,在CPython解释器中执行时,都会先锁住自己的线程,阻止别的线程执行。
当然,CPython会做一些小把戏,轮流执行Python线程。这样一来,用户看到的就是“伪并行”---python线程在交错执行,来模拟真正并行的线程。
为什么cpython需要GIL呢?这其实和cpython的实现有关。
cpython使用引用计数来管理内存,所以python脚本中创建的实例,都会有一个引用计数,来记录有多少个指针指向它。当引用计数只有0时,则会自动释放内存。
>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3
这个例子中,a的引用计数是3,因为有a,b和作为参数传递的getrefcount这三个地方,都引用了一个空列表。
这样一来,如果有两个python线程同时引用了a,就会造成引用计数的race condition,引用计数可能最终只增加1,这样就会造成内存被污染。因为第一个线程结束时,会把引用计数减少1,这是可能达到条件释放内存,当第二个线程再试图访问a时,就找不到有效的内存了。
所以说,cpython引用GIL其实主要就是这么两个原因:
1.是设计者为了规避类似于内存管理这样的复杂的竞争风险问题(race condition);
2.是因为cpython大量使用c语言库,但大部分c语言库都不是原生线程安全的(线程安全会降低性能和增加复杂度)
GIL是如何工作的
下面这张图,就是一个GIL在python程序的工作示例。其中,Thread1, 2, 3轮流执行,每一个线程在开始执行时,都会锁住GIL,以阻住别的线程执行;同样的,每一个线程执行完一段后,会释放GIL,以允许别的线程开始利用资源。
为什么python线程会去主动释放GIL呢?毕竟,如果仅仅是要求python线程在开始执行时锁住GIL,而永远不去释放GIL,那别的线程就都没有了运行的机会。
cpython中还有另一个机制,叫做check_interval,意思是cpython解释器会去轮询检查GIL的锁住情况。每隔一段时间,python解释器就会强制当前线程去释放GIL,这样别的线程才能有执行的机会。
不同版本的 Python 中,check interval 的实现方式并不一样。早期的 Python 是 100 个 ticks,大致对应了 1000 个 bytecodes;而 Python 3 以后,interval 是 15 毫秒。当然,我们不必细究具体多久会强制释放 GIL,这不应该成为我们程序设计的依赖条件,我们只需明白,CPython 解释器会在一个“合理”的时间范围内释放 GIL 就可以了。
整体来说,每一个 Python 线程都是类似这样循环的封装:for (;;) {
if (--ticker < 0) {
ticker = check_interval;
/* Give another thread a chance */
PyThread_release_lock(interpreter_lock);
/* Other threads may run now */
PyThread_acquire_lock(interpreter_lock, 1);
}
bytecode = *next_instr++;
switch (bytecode) {
/* execute the next instruction ... */
}
}
从这段代码中,我们可以看到,每个 Python 线程都会先检查 ticker 计数。只有在 ticker 大于 0 的情况下,线程才会去执行自己的 bytecode。
GIL与多线程的关系
GIL 只支持单线程,而python支持多线程,这两者之间究竟是什么关系呢?
其实,GIL的存在与pytohn支持多线程并不矛盾。GIL指的是同一时刻,程序只能有一个线程运行;而python中的多线程,是指多个线程交替执行,造成一个“伪并行”的结果,但是具体到某一时刻,仍然只有1个线程在运行,并不是真正的多线程并行。这个机制,可以看下面来表示:
例如说,我用10个线程来爬取50个网站的内容。线程1在爬取第一个网站时,被I/O block住了,处于等待状态;这时,GIL就会释放,而线程2就会开始执行,去爬取第2个网站,依次类推。等到线程1的I/O操作完成时,主程序便又会切回线程1,让其完成剩下的操作,这样一来,从用户的角度看到的,便是我们所说的多线程。