聊聊Python中的GIL

 对于广大写Python的人来说,GIL(Global Interpreter Lock, 全局解释器锁)肯定不陌生,但未必清楚GIL的历史和全貌是怎样的,今天我们就来梳理一下GIL。

1. 什么是GIL

GIL的全称是 Global Interpreter Lock,全局解释器锁。之所以叫这个名字,是因为Python的执行依赖于解释器。Python最初的设计理念在于,为了解决多线程之间数据完整性和状态同步的问题,设计为在任意时刻只有一个线程在解释器中运行。而当执行多线程程序时,由GIL来控制同一时刻只有一个线程能够运行。即Python中的多线程是表面多线程,也可以理解为fake多线程,不是真正的多线程。

可能有的同学会问,同一时刻只有一个线程能够运行,那么是怎么执行多线程程序的呢?其实原理很简单:解释器的分时复用。即多个线程的代码,轮流被解释器执行,只不过切换的很频繁很快,给人一种多线程“同时”在执行的错觉。聊的学术化一点,其实就是“并发”。

再拓展一点“并发”和“并行”的概念:

普通解释:
并发:交替做不同事情的能力
并行:同时做不同事情的能力
专业术语:
并发:不同的代码块交替执行
并行:不同的代码块同时执行

那么问题来了,Python为什么要如此设计呢?即为什么要保证同一时刻只有一个线程在解释器中运行呢

答案是为了进程安全

 

2. 什么是线程安全?

我们首先要搞清楚什么是进程,什么是线程。进程是系统资源分配的最小单位,线程是程序执行的最小单位

举一个例子,如果我们把跑程序比作吃饭,那么进程就是摆满了饭菜的桌子,线程就是吃饭的那个人。

在多线程环境中,当各线程不共享数据的时候,那么一定是线程安全的。问题是这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。

线程安全一般都涉及到synchronized,就是多线程环境中,共享数据同一时间只能有一个线程来操作 不然中间过程可能会产生不可预制的结果

接着刚才的例子,桌子上有三碗米饭,一个人正在吃,吃了两碗米饭,但是还没有吃完,因此桌子上米饭的数量还没有更新;此时第二个人也想吃米饭,如果没有线程安全方面的考虑,第二个人要是想直接拿三碗米饭吃,就会出错。

以下是这种情况的代码示例:

复制代码
n = 0   def foo():     global n     n += 1
复制代码

我们可以看到这个函数用 Python 的标准 dis 模块编译的字节码:

复制代码
>>> import dis >>> dis.dis(foo) LOAD_GLOBAL              0 (n) LOAD_CONST               1 (1) INPLACE_ADD STORE_GLOBAL             0 (n)
复制代码

代码的一行中, n += 1,被编译成 4 个字节码,进行 4 个基本操作:

  1. 将 n 值加载到堆栈上
  2. 将常数 1 加载到堆栈上
  3. 将堆栈顶部的两个值相加
  4. 将总和存储回 n
记住,一个线程每运行 1000 字节码,就会被解释器打断夺走 GIL 。如果运气不好,这(打断)可能发生在线程加载 n 值到堆栈期间,以及把它存储回 n 期间。很容易可以看到这个过程会如何导致更新丢失:
复制代码
threads = [] for i in range(100):     t = threading.Thread(target=foo)     threads.append(t)   for t in threads:     t.start()   for t in threads:     t.join()   print(n)
复制代码
通常这个代码输出 100,因为 100 个线程每个都递增 n 。但有时你会看到 99 或 98 ,如果一个线程的更新被另一个覆盖。
 
所以,尽管有 GIL,你仍然需要加锁来保护共享的可变状态:
复制代码
n = 0 lock = threading.Lock()   

                    
                
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信