python GIL 详解

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解释器无法控制线程调度,所以只能期望更快的切换线程,让主线程得以运行。下图是多线程调度模型:


python GIL 详解_第1张图片

cpu型任务

对于cpu型的任务,解释器会定时的执行check动作,进行线程的切换。这里的定时单位是tick,tick是python解释器的一个指令运行时间。python指令可以通过dis模块查看。


python GIL 详解_第2张图片

在定时check期间,线程会释放和获取GIL,在main thread中如果有待处理的signals,会进行处理。如下图:


python GIL 详解_第3张图片

下面是一段简单的计数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.

你可能感兴趣的:(python GIL 详解)