展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > Python中的GIL是什么

Python中的GIL是什么

为什么需要GILGIL本质上是一把锁,学过操作系统的同学都知道锁的引入是为了避免并发访问造成数据的不一致。CPython中有很多定义在函数外面的全局变量,比如内存管理中的usable_arenas和usedpools,如果多个线程同时申请内存就可能同时修改这些变量,造成数据错乱。另外Python的垃圾回收机制是基于引用计数的,所有对象都有一个ob_refcnt字段表示当前有多少变量会引用当前对象,变量赋值、参数传递等操作都会增加引用计数,退出作用域或函数返回会减少引用计数。同样地,如果有多个线程同时修改同

本文目录

    为什么需要 GIL

    GIL 本质上是一把锁,学过操作系统的同学都知道锁的引入是为了避免并发访问造成数据的不一致。CPython 中有很多定义在函数外面的全局变量,比如内存管理中的 usable_arenas 和 usedpools,如果多个线程同时申请内存就可能同时修改这些变量,造成数据错乱。另外 Python 的垃圾回收机制是基于引用计数的,所有对象都有一个 ob_refcnt字段表示当前有多少变量会引用当前对象,变量赋值、参数传递等操作都会增加引用计数,退出作用域或函数返回会减少引用计数。同样地,如果有多个线程同时修改同一个对象的引用计数,就有可能使 ob_refcnt 与真实值不同,可能会造成内存泄漏,不会被使用的对象得不到回收,更严重可能会回收还在被引用的对象,造成 Python 解释器崩溃。

    GIL 的实现

    CPython 中 GIL 的定义如下

    struct _gil_runtime_state {
        unsigned long interval; // 请求 GIL 的线程在 interval 毫秒后还没成功,就会向持有 GIL 的线程发出释放信号
        _Py_atomic_address last_holder; // GIL 上一次的持有线程,强制切换线程时会用到
        _Py_atomic_int locked; // GIL 是否被某个线程持有
        unsigned long switch_number; // GIL 的持有线程切换了多少次
        // 条件变量和互斥锁,一般都是成对出现
        PyCOND_T cond;
        PyMUTEX_T mutex;
        // 条件变量,用于强制切换线程
        PyCOND_T switch_cond;
        PyMUTEX_T switch_mutex;
    };

    最本质的是 mutex 保护的 locked 字段,表示 GIL 当前是否被持有,其他字段是为了优化 GIL 而被用到的。线程申请 GIL 时会调用 take_gil() 方法,释放 GIL时 调用 drop_gil() 方法。为了避免饥饿现象,当一个线程等待了 interval 毫秒(默认是 5 毫秒)还没申请到 GIL 的时候,就会主动向持有 GIL 的线程发出信号,GIL 的持有者会在恰当时机检查该信号,如果发现有其他线程在申请就会强制释放 GIL。这里所说的恰当时机在不同版本中有所不同,早期是每执行 100 条指令会检查一次,在 Python 3.10.4 中是在条件语句结束、循环语句的每次循环体结束以及函数调用结束的时候才会去检查。

    申请 GIL 的函数 take_gil() 简化后如下

    static void take_gil(PyThreadState *tstate)
    {
        ...
        // 申请互斥锁
        MUTEX_LOCK(gil->mutex);
        // 如果 GIL 空闲就直接获取
        if (!_Py_atomic_load_relaxed(&gil->locked)) {
            goto _ready;
        }
        // 尝试等待
        while (_Py_atomic_load_relaxed(&gil->locked)) {
            unsigned long saved_switchnum = gil->switch_number;
            unsigned long interval = (gil->interval >= 1 ? gil->interval : 1);
            int timed_out = 0;
            COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out);
            if (timed_out &&  _Py_atomic_load_relaxed(&gil->locked) && gil->switch_number == saved_switchnum) {
                SET_GIL_DROP_REQUEST(interp);
            }
        }
    _ready:
        MUTEX_LOCK(gil->switch_mutex);
        _Py_atomic_store_relaxed(&gil->locked, 1);
        _Py_ANNOTATE_RWLOCK_ACQUIRED(&gil->locked, /*is_write=*/1);
    
        if (tstate != (PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) {
            _Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate);
            ++gil->switch_number;
        }
        // 唤醒强制切换的线程主动等待的条件变量
        COND_SIGNAL(gil->switch_cond);
        MUTEX_UNLOCK(gil->switch_mutex);
        if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request)) {
            RESET_GIL_DROP_REQUEST(interp);
        }
        else {
            COMPUTE_EVAL_BREAKER(interp, ceval, ceval2);
        }
        ...
        // 释放互斥锁
        MUTEX_UNLOCK(gil->mutex);
    }

    整个函数体为了保证原子性,需要在开头和结尾分别申请和释放互斥锁 gil->mutex。如果当前 GIL 是空闲状态就直接获取 GIL,如果不空闲就等待条件变量 gil->cond interval 毫秒(不小于 1 毫秒),如果超时并且期间没有发生过 GIL 切换就将 gil_drop_request 置位,请求强制切换 GIL 持有线程,否则继续等待。一旦获取 GIL 成功需要更新 gil->locked、gil->last_holder 和 gil->switch_number 的值,唤醒条件变量 gil->switch_cond,并且释放互斥锁 gil->mutex。

    释放 GIL 的函数 drop_gil() 简化后如下

    static void drop_gil(struct _ceval_runtime_state *ceval, struct _ceval_state *ceval2,
             PyThreadState *tstate)
    {
        ...
        if (tstate != NULL) {
            _Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate);
        }
        MUTEX_LOCK(gil->mutex);
        _Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1);
        // 释放 GIL
        _Py_atomic_store_relaxed(&gil->locked, 0);
        // 唤醒正在等待 GIL 的线程
        COND_SIGNAL(gil->cond);
        MUTEX_UNLOCK(gil->mutex);
        if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request) && tstate != NULL) {
            MUTEX_LOCK(gil->switch_mutex);
            // 强制等待一次线程切换才被唤醒,避免饥饿
            if (((PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) == tstate)
            {
                assert(is_tstate_valid(tstate));
                RESET_GIL_DROP_REQUEST(tstate->interp);
                COND_WAIT(gil->switch_cond, gil->switch_mutex);
            }
            MUTEX_UNLOCK(gil->switch_mutex);
        }
    }

    首先在 gil->mutex 的保护下释放 GIL,然后唤醒其他正在等待 GIL 的线程。在多 CPU 的环境下,当前线程在释放 GIL 后有更高的概率重新获得 GIL,为了避免对其他线程造成饥饿,当前线程需要强制等待条件变量 gil->switch_cond,只有在其他线程获取 GIL 的时候当前线程才会被唤醒。

    几点说明

    GIL 优化

    受 GIL 约束的代码不能并行执行,降低了整体性能,为了尽量降低性能损失,Python 在进行 IO 操作或不涉及对象访问的密集 CPU 计算的时候,会主动释放 GIL,减小了 GIL 的粒度,比如

    • 读写文件

    • 网络访问

    • 加密数据/压缩数据

    所以严格来说,在单进程的情况下,多个 Python 线程时可能同时执行的,比如一个线程在正常运行,另一个线程在压缩数据。

    用户数据的一致性不能依赖 GIL

    GIL 是为了维护 Python 解释器内部变量的一致性而产生的锁,用户数据的一致性不由 GIL 负责。虽然 GIL 在一定程度上也保证了用户数据的一致性,比如 Python 3.10.4 中不涉及跳转和函数调用的指令都会在 GIL 的约束下原子性的执行,但是数据在业务逻辑上的一致性需要用户自己加锁来保证。

    下面的代码用两个线程模拟用户集碎片得奖

    from threading import Thread
    
    def main():
        stat = {"piece_count": 0, "reward_count": 0}
        t1 = Thread(target=process_piece, args=(stat,))
        t2 = Thread(target=process_piece, args=(stat,))
        t1.start()
        t2.start()
        t1.join()
        t2.join()
        print(stat)
    
    def process_piece(stat):
        for i in range(10000000):
            if stat["piece_count"] % 10 == 0:
                reward = True
            else:
                reward = False
            if reward:
                stat["reward_count"] += 1
            stat["piece_count"] += 1
    
    if __name__ == "__main__":
        main()

    假设用户每集齐 10 个碎片就能得到一次奖励,每个线程收集了 10000000 个碎片,应该得到 9999999 个奖励(最后一次没有计算),总共应该收集 20000000 个碎片,得到 1999998 个奖励,但是在我电脑上一次运行结果如下

    {'piece_count': 20000000, 'reward_count': 1999987}

    总的碎片数量与预期一致,但是奖励数量却少了 12 个。碎片数量正确是因为在 Python 3.10.4 中,stat["piece_count"] += 1 是在 GIL 约束下原子性执行的。由于每次循环结束都可能切换执行线程,那么可能线程 t1 在某次循环结束时将 piece_count 加到 100,但是在下次循环开始模 10 判断前,Python 解释器切换到线程 t2 执行,t2 将 piece_count 加到 101,那么就会错过一次奖励。

    附:如何避免受到GIL的影响

    说了那么多,如果不说解决方案就仅仅是个科普帖,然并卵。GIL这么烂,有没有办法绕过呢?我们来看看有哪些现成的方案。

    用multiprocess替代Thread

    multiprocess库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。

    当然multiprocess也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocess由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。具体难点在哪有兴趣的读者可以扩展阅读这篇文章

    用其他解析器

    之前也提到了既然GIL只是CPython的产物,那么其他解析器是不是更好呢?没错,像JPython和IronPython这样的解析器由于实现语言的特性,他们不需要GIL的帮助。然而由于用了Java/C#用于解析器实现,他们也失去了利用社区众多C语言模块有用特性的机会。所以这些解析器也因此一直都比较小众。毕竟功能和性能大家在初期都会选择前者,Done is better than perfect。

    所以没救了么?

    当然Python社区也在非常努力的不断改进GIL,甚至是尝试去除GIL。并在各个小版本中有了不少的进步。有兴趣的读者可以扩展阅读这个Slide

    另一个改进Reworking the GIL

    – 将切换颗粒度从基于opcode计数改成基于时间片计数

    – 避免最近一次释放GIL锁的线程再次被立即调度

    – 新增线程优先级功能(高优先级线程可以迫使其他线程释放所持有的GIL锁)

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Python
    相关文章 更多
    精品专题 更多
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw macOS版
    AionClaw macOS版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy macOS版
    Loomy macOS版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。

    AirBuddy 3 macOS版
    AirBuddy 3 macOS版

    AirBuddy 3 是面向 macOS 的蓝牙外设增强工具,可集中查看 AirPods、Beats 与苹果设备电量,提供快速连接、播放控制、电量提醒、桌面小组件和多台 Mac 外设接力等功能。

    AirServer macOS版
    AirServer macOS版

    AirServer macOS版可把 Mac 转换为无线投屏接收端,通过 AirPlay 或 Google Cast 接收手机、平板和电脑画面,并支持多设备同时连接、高清镜像及会话录制,适合家庭娱乐、课堂教学、会议演示和应用展示。

    AnyDesk macOS版
    AnyDesk macOS版

    AnyDesk macOS版是一款适用于远程办公、设备维护和客户支持的远程桌面工具,可连接并操作异地计算机,支持文件传输、无人值守访问、会话权限控制及跨平台协作。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。

    讯飞绘文 网页版
    讯飞绘文 网页版

    讯飞绘文网页版是科大讯飞推出的AI图文创作与内容运营工具,覆盖热点选题、文章生成、稿件校对、智能配图、排版发布和数据复盘等环节,适合自媒体创作者及内容团队在线使用。

    Maya(Autodesk Maya) Windows版
    Maya(Autodesk Maya) Windows版

    Autodesk Maya 是面向影视、电视、动画和游戏行业的专业三维制作软件,集多边形与 NURBS 建模、角色绑定、关键帧和动作捕捉动画、Bifrost 特效模拟、材质开发及 Arnold 渲染于一体,

    Aperty Windows版
    Aperty Windows版

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw Windows版
    AionClaw Windows版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy Windows版
    Loomy Windows版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。