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

1c9e49da4f68

cpu型任务

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

1c9e49da4f68

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

1c9e49da4f68

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值