python gil_浅谈Python GIL

谈到Python,大多数人的印象都是简单、实用但是多线程效率不高,而导致这点的罪魁祸首就是---GIL(Global Interpreter Lock,全局解释器锁)。接下来给大家揭秘GIL的神秘面纱。

一、Python多线程

首先我们来进行一个cpu heavy的任务,做一个大数字的自减

最基本的单线程版本如下:

def Decrement(n):

while n > 0:

n -= 1

Decrement(100000000)

运行这段代码,我们得到时间为:

Wall time: 7.5 s

我们来运行一个多线程的版本:

from threading import Thread

def multi_version(n):

t1 = Thread(target=Decrement, args=(n // 2,))

t2 = Thread(target=Decrement, args=(n // 2,))

t1.start()

t2.start()

t1.join()

t2.join()

multi_version(100000000)

运算时间为:

Wall time: 7.19 s

我们会发现,实际上与单线程的版本所运算的时间并没有大的差别,速度并没有想象中的提升一倍。

基于此,我们会自然而然的想到,Python的多线程并没有起到并行计算的作用,难道Python的线程是假的线程?

实际上,Python的线程,的的确确是封装的底层的操作系统的线程,并且,Python的线程也完完全全的受到操作系统管理,但是,由于解释器的C语言实现部分在完全并行执行时并不是线程安全的。因此,解释器被一个全局解释器锁保护着,能确保任何时候都只能一个Python线程执行。这样就导致,Python的多线程并不能利用到多核CPU的优势,如我们上面的例子中,使用了多线程的计算密集型程序只会在单个CPU上面运行。

不过,解释器在执行时,会轮流执行Python线程,使线程交错执行,来模拟真正并行的线程。

说了这么多,那不禁要问:

为什么需要GIL?

这与Cpython的实现有关,Cpython是当下最流行的Python的解释器,使用引用计数来管理内存,在Python中,一切都是对象,引用计数就是指向对象的指针数,当这个数字变成0,则会进行垃圾回收,自动释放内存。

我们来看一个例子:

import sys

a = [1]

b = a

c = b

print(f"引用a {sys.getrefcount(a)}次")

结果为:引用a 4次

这个例子中,因为a、b、c、getrefcount都有引用[1]这个列表,所以是四次。

那我们想一下,如果有两个线程,同时引用a,这样就有可能a的引用计数只增加了一次,这就会导致内存被污染了,因为当第一个线程结束的时候,a的引用计数减去1,而如果这时候a的引用计数刚好为0的时候,a所引用的列表就会被释放,这时候另一个线程去访问a的时候,就找不到有效的内存了。GIL的工作方式

线程1、2、3都会轮流执行,每一个线程执行前都会acquire GIL即锁住线程,然后运行完再release GIL释放线程。而释放线程的时机由python的另一个机制----check_interval来决定,该机制会轮询检查线程GIL的锁住情况,并每隔一段“合理的”时间强制正在运行的当前线程去释放GIL以让其他线程能够执行。

二、线程安全

虽然说Python有了GIL,但是并不意味着我们编写Python的程序的时候就不需要去注重线程安全了,因为,虽然说Python线程执行会有GIL来锁住线程,但是也因为check_interval机制,同样还是会导致线程安全的问题,talk is cheap,let’s look at the code.

import threading

num = 0

def change_num(n):

global num

num += n

num -= n

def run(n):

for _ in range(100000):

change_num(n)

def main():

thread1 = threading.Thread(target=run, args=[5])

thread2 = threading.Thread(target=run, args=[6])

thread1.start()

thread2.start()

thread1.join()

thread2.join()

main()

print(num)

当循环的次数够大,上述代码每次运行结果都不一致,如这次的结果就是-6

这又是为什么呢?

原因是在高级语音中,一条语句在CPU执行中实际上时若干条语句,即使是简单的num += n,也是由多条bytecode组成:

便于理解,我们就将这个过程拆解成两步:计算num + n的值

将计算出来的值赋值给num

也就是:

tmp = num + n

num = tmp

在程序正常运行的过程中,我们的代码应该是:

tmp1 = num + 5 --->tmp1 = 0 + 5 = 5

num = tmp1 --->num = tmp1 = 5

tmp1 = num - 5 --->tmp1 = 5 - 5 = 0

num = tmp1 --->num = tmp1 = 0

tmp2 = num + 6 --->tmp2 = 0 + 6 = 6

num = tmp2 --->num = tmp2 = 6

tmp2 = num - 6 --->tmp2 = 6 - 6 = 0

num = tmp2 --->num = tmp2 = 0

这种情况下,num的值是正确的,就是0

但是由于Python的线程是交替运行的,所以可能的运行过程是这样的:

tmp1 = num + 5 --->tmp1 = 0 + 5 = 5

tmp2 = num + 6 --->tmp2 = 0 + 6 = 6

num = tmp2 --->num = tmp2 = 6

num = tmp1 --->num = tmp1 = 5

tmp1 = num - 5 --->tmp1 = 5 - 5 = 0

num = tmp1 --->num = tmp1 = 0

tmp2 = num - 6 --->tmp2 = 0 - 6 = -6

num = tmp2 --->num = tmp2 = -6

这样结果就是-6了。

所以也不能因为GIL就完全不注重race condition问题,使用Python多线程还是要加锁,threading模块的lock()方法,这样结果就不会出现差错了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值