GIL介绍
python全局解释器锁(global interpreter lock, GIL)限制了任何时候只能有一个thread处于运行状态,这对于cpu密集型和多线程程序并不友好,会带来性能瓶颈。
GIL解决的问题
python用引用计数来管理内存对象。当对象的引用计数变量为0的时候,对象占用的内存方可释放。引用计数变量是一个竞态条件,多个线程同时访问的时候需要进行互斥。如果不互斥,可能导致内存泄漏。
这个问题可以通过所有对象加锁来解决,但是这会导致死锁,性能等其他更复杂的问题。
GIL是给解释器自身加锁,任何python代码的执行都需要先获得解释器锁。这就解决了死锁问题(只有一个锁),并且不会带来额外的性能问题。但是却导致cpu型的任务,任意时刻只能同时运行一个thread。
GIL并不是这个问题的唯一解决方案。线程安全的内存管理除了引用计数,也可以通过垃圾回收机制解决。但是这样会移除GIL带来的优势,单线程程序和IO型多线程程序的性能损失。
为什么选择GIL
python的设计就是简单易用,快速开发,让更多的开发者参与其中。
很多extensions需要GIL的线程安全内存管理。一些不是线程安全的C libraries可以很容易的集成到python中。并且GIL的实现很简单。对于单线程的程序也有性能的提升, GIL也是促使python如此流行的一个因素。
对于多线程程序的影响
在cpu型多线程程序中,GIL阻止了线程的并行执行。对于IO型的程序,GIL并没有太大的影响,因为当等待IO操作的时候,会进行线程切换,锁是在线程之间共享的。
GIL为什么没有被移除
移除GIL存在遗留的兼容性问题,还有很多C extensions依赖于GIL的方案。新的方案替代GIL,也会损失单线程程序和IO型多线程程序的性能,没人会希望新的版本反而导致已有程序的性能下降。
怎么解决GIL带来的影响
利用多进程模块multiprocessing。多进程会带来显著的性能提升,但是不是成倍的,因为进程比线程更重,有其他开销。GIL存在于CPython,如果条件允许,也可以尝试用其他语言实现的python版本,譬如java实现的版本Jython。
深入理解GIL
pyhton线程
python线程是真正的系统线程,posix threads(pthreads), windows threads。
完全由os管理。线程在运行时候持有GIL,在等待IO操作的时候会释放GIL。
python并没有自己的线程调度机制,所有的线程调度依赖于OS。这里会有另一个问题,就是signal的处理,signal只能在main thread中被处理,而python解释器无法控制线程调度,所以只能期望更快的切换线程,让主线程得以运行。下图是多线程调度模型:
cpu型任务
对于cpu型的任务,解释器会定时的执行check动作,进行线程的切换。这里的定时单位是tick,tick是python解释器的一个指令运行时间。python指令可以通过dis模块查看。
在定时check期间,线程会释放和获取GIL,在main thread中如果有待处理的signals,会进行处理。如下图:
下面是一段简单的计数cpu型程序,利用了run_time装饰器打印程序执行时间。
从测试结果来看,单核cpu上单线程要优于双线程,在双核cpu上结果差距更大。
所以在cpu型程序中,由于GIL的存在,单线程效率会更高。这也是GIL一直未被移除的一个因素。
# -*- coding:utf-8 -*-
import time
from functools import wraps
from threading import Thread
def run_time(fn):
@wraps(fn)
def print_run_time(*args, **kwargs):
start_time = time.time()
result = fn(*args, **kwargs)
end_time = time.time()
print(fn.__name__ + " took " + str(end_time - start_time) + " seconds.")
return result
return print_run_time
def count(n):
while n > 0:
n -= 1
@run_time
def one_thread(n):
count(n)
@run_time
def two_thread(n):
t1 = Thread(target=count, args=(n/2,))
t2 = Thread(target=count, args=(n/2,))
t1.start()
t2.start()
t1.join()
t2.join()
if __name__ == "__main__":
count_number = 400000000
one_thread(count_number)
two_thread(count_number)
cpu类型同为Intel(R) Xeon(R) CPU E5-2660 0 @ 2.20GHz
单核cpu测试结果:
one_thread took 52.9154109955 seconds.
two_thread took 54.3112771511 seconds.
双核cpu测试结果:
one_thread took 51.9967029095 seconds.
two_thread took 68.8160979748 seconds.