GIL 和 线程锁

长久以来,一直有一个问题困惑着我 :为什么 CPython 解释器中有全局解释器锁了还需要线程锁呢?

GIL 怎么理解?
看下官方解释
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe.

简单理解就是为了防止多个线程同时执行 字节码。有了这把大锁,CPU一次只能执行一条字节码。
因此,在 CPython 中,GIL 的意义就是保证字节码的安全性。 一条字节码就是一个原子操作。

众所周知,我们编写的Python代码并不是最终的执行代码,是要现经过编译为字节码才能被解释器执行。 但是,我们编写的代码中,一条语句并不是对应一条字节码,而是很多条。我们可以使用dis模块查看。

1
2
3
4
5
6
7
8
import dis
dis.dis("a+=2")
  1           0 LOAD_NAME                0 (a)
              2 LOAD_CONST               0 (2)
              4 INPLACE_ADD
              6 STORE_NAME               0 (a)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE

很明显,a+=2这条语句被编译为了6条字节码。因此在 CPython 中,这条语句就不是 原子操作,在多线程情况下就可能发成错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from threading import Thread

num = 0

def add():
    global num
    for _ in range(1000000):
        num += 1


def reduce():
    global num
    for _ in range(1000000):
        num -= 1


if __name__ == '__main__':
    t1 = Thread(target=add)
    t2 = Thread(target=reduce)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(num)

多运行几次,会发现数值会不同。
CPython 在解释字节码时,每执行 100 条字节码 或 执行时长达到 5 ms,就会随机切换线程(也可能还是旧线程),当多条线程操作同一个数据时, 如果操作的代码不能依次执行,中间被插入了其他操作数据的代码,那么这时就会出现错误。

可以想象这么一个场景:线程A 和 线程B 同时操作全局变量 X=5, 在解释器切换前 线程A 刚好加载 X,但是还没进行修改操作,这时切换到了线程B,线程B 首先加载 X,这时 X 并没有变化,仍为5,之后线程B 对 X 进行了 加一操作,此时 X=6, 然后时间刚好到 5 ms,又被切换到线程A,线程A 继续刚才的字节码,此时的X 在 线程B 修改以前已经被加载了,所以此时的X的值 仍然是原先的值5,并没有加一。 这时如果在线程A中对X 减一,此时 X 的值会变为4。这就出现问题了呀,理想中的结果应该是5 ,不变才对呀。

GIL 只是保证了一条字节码的安全性,但是不能保护我们编写的代码(块)的安全。这里的安全是指:代码连续的执行。而使用线程锁就能让我们保护代码连续的执行。 这是我理解的 GIL 和 线程锁的不同之处。

个人见解,大都是从网上的只言片语理解的。如果有错误的地方,欢迎邮件指出来:leiyang_ace@163.com,这个问题真真困惑我很久。