返回顶部
首页 > 资讯 > 后端开发 > Python >Python中的GIL怎么实现
  • 236
分享到

Python中的GIL怎么实现

2023-07-06 11:07:40 236人浏览 泡泡鱼

Python 官方文档:入门教程 => 点击学习

摘要

本篇内容介绍了“python中的GIL怎么实现”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!为什么需要 GILGIL 本质上是一把锁,学过操

本篇内容介绍了“python中的GIL怎么实现”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

为什么需要 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, 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, 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 Threaddef 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"] += 1if __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锁)

“Python中的GIL怎么实现”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!

--结束END--

本文标题: Python中的GIL怎么实现

本文链接: https://lsjlt.com/news/358067.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Python中的GIL怎么实现
    本篇内容介绍了“Python中的GIL怎么实现”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!为什么需要 GILGIL 本质上是一把锁,学过操...
    99+
    2023-07-06
  • Python中怎么切换GIL
    这篇文章给大家分享的是有关Python中怎么切换GIL的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。python是什么意思Python是一种跨平台的、具有解释性、编译性、互动性和面向对象的脚本语言,其最初的设计是...
    99+
    2023-06-14
  • Python中的GIL是什么
    为什么需要 GILGIL 本质上是一把锁,学过操作系统的同学都知道锁的引入是为了避免并发访问造成数据的不一致。CPython 中有很多定义在函数外面的全局变量,比如内存管理中的 usable_arenas 和 usedpools,如果多个线...
    99+
    2023-05-14
    Python gil
  • python GIL怎么用
    这篇文章主要介绍了python GIL怎么用,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。python有哪些常用库python常用的库:1.requesuts;2.scrap...
    99+
    2023-06-14
  • Python中什么是GIL
    本篇文章给大家分享的是有关Python中什么是GIL,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。问:说说Python中的GIL是什么?答:在Python中GIL是Global...
    99+
    2023-06-19
  • Python中GIL全局解释锁如何实现
    今天小编给大家分享一下Python中GIL全局解释锁如何实现的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。1.为什么有GIL...
    99+
    2023-07-05
  • Python中GIL的作用是什么
    Python中GIL的作用是什么?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。python有哪些常用库python常用的库:1.requesuts;2.scra...
    99+
    2023-06-14
  • python中GIL的原理是什么
    本篇文章给大家分享的是有关python中GIL的原理是什么,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。Python主要用来做什么Python主要应用于:1、Web开发;2、数...
    99+
    2023-06-07
  • python中gil是什么意思
    这篇文章主要介绍了python中gil是什么意思,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。Python的优点有哪些1、简单易用,与C/C++、Java、C# 等传统语言相...
    99+
    2023-06-14
  • 怎么在python中获取和释放GIL
    这篇文章给大家介绍怎么在python中获取和释放GIL,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。python的五大特点是什么python的五大特点:1.简单易学,开发程序时,专注的是解决问题,而不是搞明白语言本身。...
    99+
    2023-06-14
  • python中GIL的原理分析
    小编给大家分享一下python中GIL的原理分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!python是什么意思Python是一种跨平台的、具有解释性、编译性...
    99+
    2023-06-14
  • python 中GIL锁的底层原理是什么
    这篇文章将为大家详细讲解有关python 中GIL锁的底层原理是什么,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。python可以做什么Python是一种编程语言,内置了许多有效的工具,Py...
    99+
    2023-06-14
  • GIL 猎犬:追捕并发 Python 中的瓶颈
    GIL:并发 Python 中的瓶颈 GIL(全局解释器锁)是 Python 中的一种机制,它确保同一时间只有一个线程可以执行字节码。这在保证 Python 解释器的线程安全方面至关重要,但它也限制了多线程程序的并发性,特别是在涉及密集...
    99+
    2024-03-01
    GIL、Python、并发、多线程、多进程
  • python GIL锁的底层原理是什么
    本篇内容介绍了“python GIL锁的底层原理是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!释放GIL锁原理分析在单核CPU下,这种...
    99+
    2023-06-30
  • GIL 的辐射:并发 Python 中的意外后果
    Python 是一种强大且通用的编程语言,具有广泛的库和框架,使其成为数据科学、机器学习和其他计算密集型任务的热门选择。然而,Python 的并行处理能力受到全局解释器锁 (GIL) 的限制,这可能会在某些情况下导致意外后果。 GIL ...
    99+
    2024-03-01
    GIL、Python、并发、线程、意外后果
  • GIL 的实验室:探索 Python 并发性的前沿
    GIL 的工作原理 GIL 是一个 mutex 锁,它确保 Python 解释器在同一时间只能执行一个线程。这是因为 Python 的内存管理系统不是线程安全的,如果多个线程同时访问同一个对象,可能导致数据损坏或程序崩溃。 GIL 通过...
    99+
    2024-03-01
    Python、GIL、并发性、多线程、多进程
  • Python中的枚举怎么实现
    这篇文章主要介绍了Python中的枚举怎么实现的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Python中的枚举怎么实现文章都会有所收获,下面我们一起来看看吧。使用普通类直接实现枚举在Python中,枚举和我们...
    99+
    2023-07-05
  • Python中DBSCAN怎么实现
    在Python中,可以使用scikit-learn库来实现DBSCAN算法。下面是一个简单的DBSCAN算法实现的示例:```pyt...
    99+
    2023-09-12
    Python
  • Python中readlink怎么实现
    在Python中,可以使用`os.readlink()`函数来实现`readlink`操作。这个函数接受一个参数,即要读取链接的路径...
    99+
    2023-10-26
    Python readlink
  • Python中的生成器怎么实现
    本篇内容主要讲解“Python中的生成器怎么实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Python中的生成器怎么实现”吧!前言生成器很容易实现,但却不容易理解。生成器也可用于创建迭代器,...
    99+
    2023-06-29
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作