Python进阶:深入GIL(上篇)

Python进阶:深入GIL(上篇)

HackPython致力于有趣有价值的编程教学

简介

熟悉Python的人理应都听过GIL(Global Interpreter Lock,全局解释器锁)?,大概也知道它就是造成Python多线程并发其实是「伪并行」的核心原因,但依旧很多人没有深入其中,所以HackPython尝试以上、下两篇文章来阐释GIL,分别从其表现现象、对应源码以及Python对GIL改进等方面进行讨论?。

线程与进程

在讨论GIL前,先通过简单的文字描述一下线程与进程并理解其中的关系。

当我们启动一个程序时,系统中就至少启动了一个对应的进程,即一个程序至少对应一个进程?。所谓程序,它其实是一种静态的资源实体,就是一堆代码,存在于硬盘中,本身没有任何运行的含义,而进程是动态的,它是程序操作某个数据集时的动态实体,存在于内存中?。

一个进程可以包含多个线程,这些线程可以共享当前进程中的内存空间?,这种特性就出现了线程不安全的概念,即多个线程同时使用了一个空间,导致程序逻辑错误?,常见的方式就是使用锁或信号量等机制来限制公共资源的使用?。

Python多线程的伪并行

Python中可以使用「threading」模块来创建并使用多线程,为了直观比较,先试一下一个没有使用多线程的代码?,如下:

import time

def add(n):
    sum = 0
    while sum <= n:
        sum += 1
    print(f'sum:{sum}')

if __name__ == '__main__':
    start = time.time()
    add(500000000)
    print('run time: %s'%str(time.time() - start))

代码非常简单,就是一个add()方法一直做累加操作,运行结果为?:

python 5.py
sum:500000001
run time: 23.80576515197754

那我使用多线程效果会不会好一些呢?凭感觉直观而言,应该是会的?,因为上面的程序只使用了一个线程,那我开两个线程,让其同时工作,其运行时间应该短一半才对?,但事实时使用多线程后,运行时间依旧没有变动?,多线程版本的代码如下:

import threading, time

def add(n):
    sum = 0
    while sum <= n:
        sum += 1
    print(f'sum:{sum}')

if __name__ == '__main__':
    start = time.time()
    n = 500000000
    t1 = threading.Thread(target=add, args=[n//2])
    t2 = threading.Thread(target=add, args=[n//2])
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('run time: %s'%str(time.time() - start))

为了让相加的数量相近,这里每个线程只需要执行「n//2」次,使用join的目的是得等线程运行完后,再执行后续的逻辑,这里只是为了方便记录运行时间?。运行结果如下:

python 6.py
sum:250000001
sum:250000001
run time: 23.04693603515625

发现跟一开始单线程的程序在运行时间上没有什么差异,而造成这种现象的原因就是GIL?,需要注意的是,GIL只存在于通过C语言实现的Python解释器上,即CPython上?,后人为了绕过GIL的问题利用Java开发了Jpython或使用Python自己开发了自己的解释器PyPy,这些上都不存在GIL全局解释器锁的问题?,但CPython才是当前最多人使用的主流Python解释器?。

在CPython中,每一个Python线程执行前都需要去获得GIL锁?,获得该锁的线程才可以执行,没有获得的只能等待?,当具有GIL锁的线程运行完成后,其他等待的线程就会去争夺GIL锁,这就造成了,在Python中使用多线程,但同一时刻下依旧只有一个线程在运行?,所以Python多线程其实并不是「并行」的,而是「并发」?。

看到下图,图中是Python中GIL的工作实例,其中有3个线程,线程与线程之间是顺序执行的?,每个线程开始执行时都会去获得GIL,防止其他线程线程运行?,每执行完一段时间后,就会释放GIL,让别的线程可以去争夺执行权限,如果自己本身也没有执行完,则本身也会参与这次争夺?。

可以发现,Python中的线程工作一段时间后,会主动释放GIL,这是为了让其他线程都有机会执行?,而释放的时机就涉及到了「检查间隔」(check interval)机制?,在早期版本的Python中,检查机制是100ticks,而Python3后,每15毫米使用一次检查间隔,然后就会释放GIL锁?。

但需要注意的是线程有了GIL后并不意味着使用Python多线程时不需要考虑线程安全?,「GIL的存在是为了方便使用C语言编写CPython解释器的编写者,而顶层使用Python时依旧要考虑线程安全」?,在下一篇中会从原始编码层面来解释存在GIL后,依旧会有线程不安全现象的原因。

多进程实现并行

GIL的存在让Python多线程在运行CPU密集型性程序时显得非常无力,为了绕过GIL的限制,一种简单的方法就是使用多进程?,这是因为GIL只会存在于线程级别,即一个进程为了确保某一时刻下只有一个线程在运行,才使用GIL?,但多个进程之间并不会出现这种限制,不同的进程会运行在CPU不同的核上,实现真正的「并行」?。

通过进程的方式将上面的任务再执行一遍,看一下运行时长,具体代码如下:

from multiprocessing import Process
import time

def add(procname, n):
    sum = 0
    while sum <= n:
        sum += 1
    print(f'process name: {procname}')
    print(f'sum: {sum}')

if __name__ == '__main__':
    start = time.time()
    n = 500000000
    p1 = Process(target=add, args=('Proc-1',n//2))
    p2 = Process(target=add, args=('Proc-2',n//2))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print('run time: %s'%str(time.time() - start))

Python中多进程可以使用 multiprocessing 这个库,使用方法与使用线程类似?,代码中启用了两个进程,分别运行 n//2 数据量的数据,其结果如下:

python 7.py
process name: Proc-1
sum: 250000001
process name: Proc-2
sum: 250000001
run time: 12.768253087997437

从结果可以看出,时间确实减少了一半左右,多进程状态下确实是真正的「并行」。

如何绕过GIL?

有了多进程后,大部分程序都可以通过多进程的方式绕过GIL?,但如果依旧不满足,就需要使用C/C++来实现这部分代码,并生成对应的so或dll文件,再通过Python的ctypes将其调用起来?,Python中很多对计算性能有较高要求的库都采用了这种方式,如Numpy、Pandas等等?。

如果你对程序的性能要求的特别严格,此时更好的方法是选择其他语言?。

结尾

本节简单的讨论了Python中GIL相关的内容,在下一篇中会从代码层面再次深入的讨论GIL的相关内容,欢迎学习 HackPython 的教学课程并感觉您的阅读与支持。

??

参考文章:

David Beazley 的Understanding the Python GIL

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值