Python多线程(一):GIL

最近在学习Python的多线程编程,写几篇文章记录一下。

GIL是Global Interpreter Lock,即全局解释锁的缩写,保证了了同一时刻只有一个线程在一个CPU上执行字节码,无法将多个线程映射到多个CPU上。这是CPython解释器的缺陷,由于CPython是大部分环境下默认的Python执行环境,而很多库都是基于CPython编写的,因此很多人将GIL归结为Python的问题。

GIL被设计来保护线程安全,由于多线程共享变量,如果不能很好的进行线程同步,多线程非常容易将线程改乱。实际上即使有了GIL,这个问题也无法完全解决,因为GIL实际上也会释放,而且它并不是在某个线程执行完成后才释放,而是根据代码的字节码或者时间片进行释放,下面是一个例子:

import threading

total = 0
def add():
    global total
    for i in range(1000000):
        total += 1

def desc():
    global total
    for i in range(1000000):
        total -= 1

thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()

print(total)

这个程序直观来看,是将total加1000000减1000000,不管哪个线程先执行,最后的结果应该都是0才对,但是如果允许你该上面的代码多次,就会发现每次代码的结果都不一样,有正有负。这其中的原因就涉及到了GIL的释放。我们首先可以查看一下普通加法函数的字节码:

import dis
def add1(a):
    a += 1
    return a
print(dis.dis(add1))

结果如下:

  2           0 LOAD_FAST                0 (a)
              2 LOAD_CONST               1 (1)
              4 INPLACE_ADD
              6 STORE_FAST               0 (a)

  3           8 LOAD_FAST                0 (a)
             10 RETURN_VALUE
None

可以看到a += 1的执行过程是先将变量a装载进CPU,再将常量1装载进CPU,然后执行相加操作,最后再将a存储在内存中。由于GIL不是根据Python代码段来释放,而是根据字节码或者时间片来释放的,在之前的例子中,如果add函数在进行加法后还未在内存中保存,GIL释放,desc函数获得执行权,此时它进行装载时装载的变量total是未进行加法操作的total,因此相当于之前的add函数失去了作用,在进行多次循环后,程序的运行结果自然不为0。这种情况称为竞态条件(race condition),即使没有GIL,也会出现这种问题。解决方法是使用锁机制,将会在后面的文章中提到。

还有一种条件会导致GIL释放,那就是当程序遇到IO操作和time.sleep将程序阻塞的时候,因此多线程对于处理IO操作的问题非常有效。

你可能感兴趣的:(Python多线程(一):GIL)