关于python3.14中的自由线程(偏置引用计数实现)

64 阅读3分钟

CPython 的全局解释器锁(“GIL”)可以防止多个线程同时执行 Python 代码。但是对于计算密集型任何和如今普遍的多核CPU来说, 这种并发性的缺失往往比Python代码的执行速度更为严重。

PEP703就是为解决全局解释器锁(“GIL”)的问题所提出的解决方案

PEP703通过四个方面来优化全局解释器锁带来的问题:

  • Reference counting
  • Memory management
  • Container thread-safety
  • Locking and atomic APIs
  1. Reference counting(引用计数)

    CPython 的引用计数实现强依赖GIL, 使其实现线程安全。PEP703通过把原子引用计数切换到偏置引用技术来规避GIL对引用计数的影响。

    偏置引用计数(BRC)是一种技术,最早由 Jiho Choi、Thomas Shull 和 Josep Torrellas 于 2018 年提出。它基于观察到大多数对象仅由单线程访问,即使在多线程程序中也是如此。每个对象都关联一个拥有线程(创建它的线程)。来自拥有线程的引用计数使用非原子指令来修改“局部”引用计数。其他线程使用原子指令修改“共享”引用计数。

image.png 偏置引用计数实现中会使用 ob_tidob_ref_local 和 ob_ref_shared这三个字段。ob_gc_bits 被用于存储垃圾回收标志,之前存放于 PyGC_Head。  ob_mutex 字段用于在使用原子指令来修改共享引用计数的时候提供对象的锁定。

对于一些小对象,如小字符串,小整数,以及True,False和None等在程序生命周期内一直存活的对象,通过设置本地引用计数(ob_ref_local)为UINT32_MAX来保持永久化。

Py_INCREF 和 Py_DECREF 宏对于永久对象来说是没用的。这样可以避免当多个线程同时访问这些对象时,引用计数字段发生竞争。

image.png 这是Py_INCREF的代码,其中_Py_atomic_load_uint32_relaxed用来获取对象当前线程的引用计数,加1后如果是0则代表当前对象是永久对象,不做任何处理。_Py_IsOwnedByCurrentThread判断当前对象是否属于当前线程,如果是则把ob_ref_local加1。否则把ob_ref_shared高位加1。

image.png 这是Py_DECREF的代码,还是一样用_Py_atomic_load_uint32_relaxed来获取对象当前线程的引用计数,如果当前引用计数为UINT32_MAX则代表是永生对象,不做任何处理。还是用_Py_IsOwnedByCurrentThread判断当前对象是否属于当前线程, 如果是则把ob_ref_local减1,如果当前线程引用计数减为0,则判断共享引用计数是否是0,如果是0则释放当前对象,如果共享引用计数不是0,则根据一系列规则去合并引用计数,确保对象不会被gc。如果对象不属于当前线程,则通过原子操作去修改共享引用计数。