GIL, 全称: Global Interpreter Lock (全局解释器锁), 是解释器采用的一种机制,
它的作用是:确保同一时刻只有一个线程在执行。
回顾 操作系统、CPU、线程的关系
线程包含需要 CPU 执行的指令集合,线程需要参加 操作系统的线程调度,才能获得CPU的使用时间, 即时间片。
GIL 的设计与运行原理
1、线程创建:直接使用 ”操作系统调度算法“, 使用操作系统的原生线程。
2、线程的切换 和 全局解释器锁:
不能把 python线程 全权交给操作系统调度,因为操作系统是按时间片切换线程的,CPU只会机械地在时间片内执行
一定量的指令,然而,一个 python对象 的基本操作(如赋值),包含的指令数量很可能比这要大,在一个时间片内,
CPU很可能没法执行完。这就会导致线程安全问题。
为此,作者设定了一些条件,使得一些 基本的对象操作, 能够保持其原子性。 那就是给整个解释器加一把锁,
从而确保 每次只有一个线程能工作 ,只有在达成一定的条件时,才会释放锁,让其他线程工作
可以想象成,把多个线程都关到一个屋子里,每次只能有一个线程出来玩,限定玩多久就必须回来,让其他线程出去玩
3、释放锁的指定条件:
条件一: 设置一个超时时间,在线程达到这个时间后,释放 GLI。
在 Python3.2之前,是通过计数实现(100个Python指令)
或条件二: 执行 I/O 操作时总是会释放 GIL。因为 I/O 操作一般都很耗时,需要等待对方响应与其阻塞等待,
不如先让其他线程工作。(所以 GIL对于 IO密集型的多线程程序影响不大)
得益于GIL,能够实现原子性的基本操作
引自 官方文档
每一条字节码指令以及每一条指令对应的 C 代码实现都是原子的。
而实际上,对内建类型(int,list,dict 等)的共享变量的“类原子”操作都是原子的。
举例来说,下面的操作是原子的(L、L1、L2 是列表,D、D1、D2 是字典,x、y 是对象,i,j 是 int 变量):
L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()
以下操作不是原子操作, 如果在多线程环境其存在共享资源的情况下,必须使用互斥锁
i = i+1
L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1
另外,覆盖其他对象的操作会在其他对象的引用计数变成 0 时触发其 del() 方法,
这可能会产生一些影响。对字典和列表进行大量操作时尤其如此。如果有疑问的话,使用互斥锁!
GIL 的缺陷
GIL 每次只允许一个线程出去参加 "操作系统的线程调度",在 ”单核处理器时代“ 并没有什么缺陷,因为在当时,
多线程仅仅是用于提高资源利用率(不让CPU闲着),并不会提高单个程序的计算效率, 所以,GIL 的设计 完全符合当时的
需求, 能够让多线程很高效地使用单核CPU。
到了 ”多核处理器时代“,多线程程序实际上可以使用多核来提高计算效率, 但 Python 因为 GIL 的存在,即使在多核
处理器下,依然每次只放出一个线程工作,这样就导致程序至始至终都只用到1个核,完全没有发挥出多核处理器的性能,
就计算效率而言,还不如单线程程序效率高。
官方尝试过优化甚至是删除GIL。优化方案没有成功,因为新的GIL严重降低了单线程的性能。另外,删除方案也很难实现,
因为当前已经有太多库依赖着GIL的特性了。
那么,其他语言,如 Java 是怎么让多线程使用多核且保证程安全的呢?
不像 Python 把一部分工作放到了解释器里,Java 直接把线程安全工作都堆到了用户层面:
1、和 Python一样实现了悲观互斥锁
2、官方实现了无阻塞乐观锁:提供了很多atomic类,利用 CPU硬件原语 CAS 实现。
GIL缺陷 解决方案
1、使用多进程: 每个进程都有自己的GIL,只要分成多进程,就能有效地使用多核了。
concurrent.futures模块中的 ProcessPoolExecutor类提供了一个简单的方法,如果想对任务分发做更多控制,
可以使用 multiprocessing 模块提供的底层 API。
2、使用C扩展: 可使用 C 拓展处理耗时较久的任务,在调用 C 代码时,释放 GIL,让其他线程执行。
zlib, hashlib 等标准库就是这样做的。
3、使用协程: 针对单线程IO密集型程序而言,io等待时间较长,可以先挂起io,先执行线程中的其他逻辑,
等收到io数据写到内存时,再处理这逻辑。
参考文档:
知乎: 浅析CPython的全局解释锁GIL
Python官方文档:
Thread State and the Global Interpreter Lock
What kinds of global value mutation are thread-safe?