Python GIL 学习笔记

什么是GIL

在CPython中,线程执行之前,必须先获取全局解释锁(GIL是一种字节码级别的互斥锁), GIL机制保证了数据修改操作的安全性。注意GIL保护的是Python 解释器的状态,比如对象的引用计数信息,如需确保用户数据对象的原子性操作,需要在用户层对数据修改进行加锁操作
GIL 定义
 

操作系统、cpu、进程、线程及其关系【转载参考资料2】

基础概念

  • 操作系统: 从操作系统与CPU之间的关系上,操作系统发送指令到CPU。
  • CPU(Central Processing Unit):执行接收到的指令。多CPU是指计算机上物理地安装了多个CPU,CPU多核是指CPU物理上包含多个内核单元。
  • 进程: 操作系统分配资源的基本单位,可以看成一个容器,可包含多个线程。
  • 线程:线程是CPU分配时间片的基本单位。线程与父进程(创建线程的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程。
  • 寄存器:是 CPU 内部数量较少但是运行速度很快的内存(与之对应的是 CPU 外部运行速度相对较慢的 RAM主内存——Random Access Memory, 随机存储器)。寄存器通过对常用值的快速访问来提高计算机程序运行的速度。
  • 程序计数器:是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存储的值为正在执行的指令的位置,或者下一个将要被执行的指令的位置。
  • 上下文:是指某一时间点 CPU 寄存器和程序计数器的内容。
  • I/O: 可以被定义为任何信息流入或流出 CPU 与主内存(RAM)。也就是说,一台电脑的 CPU和内存,与该电脑的用户(通过键盘或鼠标)、存储设备(硬盘或,从硬盘中读取,或写入到硬盘),还有其他电脑的任何交流都是 I/O。定义二:广义上只要不需要CPU参与的都是I/O操作,如人为挂起线程。
     

CPU架构与工作原理

计算机有5大基本组成部分,运算器,控制器,存储器,输入和输出。运算器、控制器封装到一起,加上寄存器组和cpu内部“总线”构成中央处理器(CPU)。因此从逻辑上,CPU可以划分为3个模块:控制单元、运算单元和存储单元,这三个部分由CPU总线连接起来。
CPU架构
CPU的运行原理就是:控制单元在时序脉冲的作用下,将指令计数器里所指向的指令地址(这个地址是在内存里的)送到地址总线上去,然后CPU将这个地址里的指令读到指令寄存器进行译码。对于执行指令过程中所需要用到的数据,会将数据地址也送到地址总线,然后CPU把数据读到CPU的内部存储单元(就是内部寄存器)暂存起来,最后命令运算单元对数据进行处理加工。周而复始,一直这样执行下去。

一台计算机可以拥有多个cpu,多cpu之间通过“总线”通讯。
多CPU通讯
一个cpu可以有多个核,不同核之间通过“L2 cache”通讯。
单CPU通讯

CPU缓存是位于CPU与内存之间的临时数据交换器,它的容量比内存小的多但是交换速度却比内存要快得多。CPU缓存一般直接跟CPU芯片集成,或位于主板总线互连的独立芯片上。

CPU内存
随着多核CPU的发展,CPU缓存通常分成了三个级别:L1,L2,L3。级别越小速度越快,同时容量越小。L1 是最低层,容量最小(例如:32K),速度最快,每个核上都有一个 L1 缓存,L1 缓存实质有两部分, 一个用于存数据的 L1d Cache(Data Cache),一个用于存指令的 L1i Cache(Instruction Cache)。L2 缓存更大一些(例如:256K),速度要慢一些, 一般情况下每个核上都有一个独立的L2 缓存。 L3 缓存是三级缓存中最大的一级(例如3MB),同时也是最慢的一级,在同一个CPU之间的核共享一个 L3 缓存。

读取数据过程,就像数据库缓存一样,首先在最快的缓存中找数据,如果缓存没有命中(Cache miss), 则往下一级找,直到三级缓存都找不到时,向内存要数据。计算过程, 程序以及数据被加载到主内存;指令和数据被加载到CPU的高速缓;CPU执行指令,把结果写到高速缓存;高速缓存中的数据写回主内存。
 

进程与线程的关系

  • 进程是容器,是操作系统分配资源的最小单位,一个程序有至少一个进程。线程是程序执行的最小单位,是CPU分配时间片的最小单位,一个进程有至少一个线程。
  • 进程有自己独立的地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。
  • 多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
     

Python中的线程、进程(参考资料一、六)

线程是负责具体的执行,而进程扮演着容器的角色,进程中会包含一个、或者多个线程,以及其它资源。基于线程、进程及其关系,下面记录Python线程模型, 与Python进程模型, 本部分的实验实验环境为Windows10,CPU信息如下(获取CPU信息通过——win+R -> wmic -> cpu get; os.cpu_count()返回8)。
CPU信息

GIL 释放机制

Python3.2前,GIL的释放逻辑是当前线程遇见I/O操作,或者ticks计数达到100,进行释放(ticks可以看作是一种特殊的计数器,专门做用于GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整。)。因为计算密集型线程在释放GIL之后,又会立即去申请GIL,并且通常在其它线程还没有调度完之前,它就已经重新获取到了GIL,就会导致一旦计算密集型线程获得了GIL,那么它在很长一段时间内都将占据GIL,甚至一直到该线程执行结束。—— 这与参考资料五的实验结果一致。

Python 3.2开始使用新的GIL,新的GIL实现中用一个固定的超时时间来指示当前的线程放弃GIL。在当前线程保持GIL,且其他线程请求GIL时,当前线程就会在5毫秒后被强制释放该锁。该改进在单核的情况下,对于单个线程长期占用GIL的情况有所好转。
 

Python 线程模型

单线程单CPU的执行逻辑按顺序执行,多线程单CPU或者多线程多CPU,按照“获取GIL -> 执行线程 -> 执行至指定数量的字节码指令,或者线程主动挂起 -> 挂起线程 -> 释放GIL -> 重复该过程” 线程切换的方式执行。线程执行之前必须先获取GIL, 并且仅有一个GIL。如下所示:
在这里插入图片描述
Python多线程模型

单CPU下,所有线程都位于同一CPU, 多CPU下, 多线程可能位于不同的CPU。相比于单CPU,多CPU存在严重的线程颠簸(thrashing)。单CPU多线程条件下,每次释放GIL,唤醒的那个线程都能获取到GIL,所以能够无缝执行。但在多CPU多线程下,CPU0释放GIL后,其它CPU上的线程都会进行竞争GIL,但GIL可能会马上又被CPU0拿到,导致其它CPU上被唤醒后的线程,会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低。

在单CPU4核条件下,创建四个独立线程,查看CPU使用率:

import threading

def f():
    while True:
        pass

if __name__ == "__main__":
    ts = [threading.Thread(target=f) for _ in range(4)]

    for t in ts:
        t.start()   # 没有join,等待线程结果,则后续的主线程会继续运行

    while True:
        pass

在这里插入图片描述
程序运行创建一个新进程与4个独立的线程,4个线程不断抢占CPU资源,稳定后,CPU利用率大约为25%。

在这里插入图片描述
并且查看任务管理器可以发现,只存在一个Python 解释器
 

Python 进程模型

Python 进程并行只能发生在多CPU中,单CPU下只能实现进程并发。在多进程中,每个进程中会维护独立的GIL。
Python 多进程模型

在单CPU4核条件下,再看创建四个独立进程的CPU占用率:

import multiprocessing

def f():
    while True:
        pass

if __name__ == "__main__":
    jobs = [multiprocessing.Process(target=f) for _ in range(4)]

    for job in jobs:
        job.start()

    while True:
        pass

在这里插入图片描述
可以看到CPU占用率接近100%,说明独立的进程的确在CPU多核中并行的执行。
在这里插入图片描述
通过展开任务管理器的Pycharm项,可以发现共计有五个Python解释器,因为有4个创建的独立进程,架上一个主进程。
 

I/O密集型 vs CPU密集型

CPU密集型操作的典型代表为“压缩、hash、解密、数据分析与计算”等,I/O密集型操作的典型代表为“文件下载、爬虫、sleep”等。 对比Python线程模型, 与Python进程模型可以发现,CPU密集型操作适合用多进程进行并行运算,而I/O密集型操作适合用多线程进行并行运算。
 

小结

GIL产生在“解决单核CPU条件下,多进程、多线程中的资源管理——引用计数问题”,随机计算机快速发展,多核CPU称为主流。但是在多核时代,由于GIL限制,某一时刻只能有一个线程执行,以此确保线程操作的安全性,牺牲了计算效率。于是基于Python线程模型, 提出Python进程模型。 通过在每个进程中,维护独立的Python解释器,以此实现并行,提高多核利用率。

虽然多进程模型是真正的并行,而多线程只是并发,但进程比线程的开销更大,并且进程之间的通信,比线程之间的通信更复杂,因此Python进程模型与 Python线程模型有着各自的优势,需要依据场景选择合适的工具。
 

参考资料

  1. 深入浅出剖析Python的全局解释锁GIL
  2. 进程,线程与多核,多cpu之间的关系
  3. 聊聊Python中的GIL
  4. 查看windows机器的cpu信息
  5. The Python GIL Visualized
  6. Python 多线程与多进程
  7. Python 术语库
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值