最近在学习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操作的问题非常有效。