《Python高级编程》11.2 嵌入式与自由线程运行时

真编译一个 C 宿主把 CPython 嵌进来,实测 Py_Initialize、PyRun_SimpleString、Py_InitializeFromConfig 的调用与返回值;再读 3.14.6 的真实头文件,对比标准构建与自由线程构建的对象布局、共享引用状态位、PyMutex 与临界区,并如实交代本机无法实测的部分。

本节目标:能用 C API 把 CPython 嵌进宿主程序并取回结果;从 3.14.6 的真实头文件读懂自由线程构建改动了什么,并准确区分「能实测」与「只能读源码」的部分。
适用版本:Python 3.12+(实测 3.14.6)

11.2 嵌入式与自由线程运行时

5.2 自由线程构建与迁移影响 回答的是「迁不迁、代价多大」;本节把镜头对准运行时本身——一头是把解释器嵌进 C 程序,另一头是 t 构建下对象与引用计数的物理布局。两者都靠直接读头文件和真编译来落地。

11.2.1 最小嵌入式解释器(真编译真跑)

用 Python 头文件写一个宿主 C 程序,把解释器当脚本引擎用:

#include <Python.h>

int main(void) {
    Py_Initialize();
    printf("Py_GetVersion: %s\n", Py_GetVersion());
    int rc = PyRun_SimpleString(
        "import sys\n"
        "print('embedded run, version =', sys.version.split()[0])\n"
        "print('gil enabled =', sys._is_gil_enabled())\n");
    printf("PyRun_SimpleString rc = %d\n", rc);
    int rc2 = PyRun_SimpleString("1/0\n");   /* 故意抛异常 */
    printf("second call rc = %d\n", rc2);
    if (Py_FinalizeEx() < 0) return 120;
    return 0;
}

编译时 python3-config --includes 给头文件路径,--ldflags --embed 给嵌入模式的链接参数(少了 --embed 会找不到 Py_Initialize):

cc embed.c -o embed $(python3-config --includes) $(python3-config --ldflags --embed)

真实输出(本机 Apple clang 16.0.0 + Homebrew 3.14.6):

Traceback (most recent call last):
  File "<string>", line 1, in <module>
ZeroDivisionError: division by zero
embedded run, version = 3.14.6
gil enabled = True
Py_GetVersion: 3.14.6 (main, Jun 10 2026, 10:03:53) [Clang 21.0.0 ...]
PyRun_SimpleString rc = 0
second call rc = -1

两个容易被忽略的点:

  • PyRun_SimpleString 用返回值报告成败:成功返回 0,脚本抛异常返回 -1(traceback 打印到 stderr,但不终止宿主)。
  • 输出顺序是乱的:traceback 先冒出来,是因为 stderr 无缓冲、而 stdout 在非 tty 下是块缓冲。嵌入时若不主动 flush,日志顺序会误导排查。

11.2.2 用 PyConfig 初始化

Py_Initialize() 是最简入口,现代嵌入更推荐 Py_InitializeFromConfig——它把初始化参数收敛成一个 PyConfig 结构,可控且失败时返回 PyStatus 而非直接 abort:

PyConfig config;
PyConfig_InitPythonConfig(&config);
config.parse_argv = 0;            /* 不解析宿主 argv */
config.optimization_level = 1;    /* 等价 python -O */
config.write_bytecode = 0;        /* 不写 .pyc */

PyStatus status = Py_InitializeFromConfig(&config);
PyConfig_Clear(&config);          /* 无论成败都要清理 */
if (PyStatus_Exception(status)) { Py_ExitStatusException(status); }
$ ./embed2
sum of squares 0..9 = 285
configured optimization_level = 1
running optimization_level   = 1

config.optimization_level 设进去后,运行期可用 Py_OptimizeFlag 读到同一个值——但它在 3.12 起已标记 deprecated(编译时报 -Wdeprecated-declarations),新代码应从 PyConfig 侧管理这项状态。

11.2.3 从 C 取回 Python 的值

嵌入最常见的需求是「跑一段 Python,把结果拿回 C」。用 __main__ 模块的 globals 字典取回对象:

PyRun_SimpleString("result = sum(i * i for i in range(10))");
PyObject *globals = PyModule_GetDict(PyImport_AddModule("__main__"));
PyObject *res     = PyDict_GetItemString(globals, "result");   /* 借引用 */
long value        = PyLong_AsLong(res);
printf("sum of squares 0..9 = %ld\n", value);                  /* 285 */

PyImport_AddModule("__main__") 与 PyDict_GetItemString 返回的都是借引用(borrowed reference),无需 Py_DECREF;PyLong_AsLong 把对象转成 C 的 long。这套「跑脚本 → 取全局名 → 转 C 类型」正是把 Python 当脚本引擎嵌进游戏、编辑器、科学计算宿主的最小骨架。

11.2.4 自由线程构建改了什么:读真实头文件

先如实交代环境:本机是标准构建,不是自由线程(t)构建,所以运行期数据都取自标准构建,机制描述则直接读 3.14.6 安装的真实头文件(比 PEP 703 的规范草案更权威)。

import sys, sysconfig
print("sys._is_gil_enabled():", sys._is_gil_enabled())          # True
print("Py_GIL_DISABLED     :", sysconfig.get_config_var("Py_GIL_DISABLED"))  # 0
print("SOABI               :", sysconfig.get_config_var("SOABI"))            # cpython-314-darwin
$ PYTHON_GIL=0 python3 -c "print('ok')"
Fatal Python error: config_read_gil: Disabling the GIL is not supported by this build

object.h 里,PyObject 的布局被 #ifndef Py_GIL_DISABLED / #else 分成两套。标准构建:

struct _object {
    union {
        PY_INT64_T ob_refcnt_full;   /* 整个 union 的 64 位视图 */
        struct {
            uint32_t ob_refcnt;      /* 引用计数(32 位) */
            uint16_t ob_overflow;    /* 溢出计数 */
            uint16_t ob_flags;       /* 标志位 */
        };
    };
    PyTypeObject *ob_type;
};

自由线程构建(同文件 #else 分支):

struct _object {
    uintptr_t    ob_tid;         /* 属主线程 id,0 表示无主(永生/已合并) */
    uint16_t     ob_flags;
    PyMutex      ob_mutex;       /* 每个对象一把 1 字节锁 */
    uint8_t      ob_gc_bits;     /* GC 状态(原在 PyGC_Head) */
    uint32_t     ob_ref_local;   /* 属主线程本地引用计数 */
    Py_ssize_t   ob_ref_shared;  /* 共享引用计数 + 状态位 */
    PyTypeObject *ob_type;
};

两点值得注意:一是连标准构建的引用计数都变了——不再是裸的 Py_ssize_t ob_refcnt,而是 uint32 + uint16 + uint16 的 union,为永生对象与溢出预留了位;二是 t 构建把对象头显著撑大(多出 tid、mutex、两套 refcount),这正是 5.2 里「单线程内存上升」的物理来源。

11.2.5 无 GIL 下的引用计数:偏向计数与共享状态位

refcount.h 给出了共享引用计数的状态机——低两位是标志位,其余位才是计数:

#define _Py_REF_SHARED_SHIFT      2
#define _Py_REF_SHARED_FLAG_MASK  0x3
#define _Py_REF_SHARED_INIT       0x0   /* 纯共享计数 */
#define _Py_REF_MAYBE_WEAKREF     0x1   /* 存在弱引用 */
#define _Py_REF_QUEUED            0x2   /* 已入合并队列 */
#define _Py_REF_MERGED            0x3   /* 已合并 */

_Py_INCREF 的分支直接体现了「偏向」:

if (_Py_IsOwnedByCurrentThread(op)) {
    _Py_atomic_store_uint32_relaxed(&op->ob_ref_local, new_local);  /* 快路径 */
} else {
    _Py_atomic_add_ssize(&op->ob_ref_shared, (1 << _Py_REF_SHARED_SHIFT)); /* 慢路径 */
}

属主线程增减引用只动自己的 ob_ref_local(relaxed 存储,不参与跨核同步);其他线程才走原子的 ob_ref_shared。 这就是「偏向引用计数」:单线程程序的计数操作几乎不退化,代价只在真正跨线程共享对象时付出。当属主线程退出或对象被跨线程频繁访问时,计数会被「合并」进共享字段(_Py_REF_MERGED),此后所有访问都走原子路径——头文件里 _PyObject_MergePerThreadRefcounts / _PyObject_DisablePerThreadRefcounting 就是这套「按线程计数 → 合并」机制的两个入口。

永生对象在 t 构建里用 ob_ref_local == UINT32_MAX(_Py_IMMORTAL_REFCNT_LOCAL)表示,彻底跳过增减。标准构建同样有永生对象(PEP 683),实测 sys.getrefcount 对 None、小整数、驻留字符串返回同一个巨大的哨兵值:

import sys
print(sys.getrefcount(None))       # 3221225472
print(sys.getrefcount(256))        # 3221225472
print(sys.getrefcount(object()))   # 3(普通对象)

派活消息里提到的 QSR 在 3.14.6 的公开头文件中检索不到对应符号(grep -rn QSR 无结果),故本节不展开这个缩写,只讲能实证的 _Py_REF_* 状态位与偏向计数机制。

11.2.6 每对象一把锁与 Python 临界区

t 构建里每个对象带一个 PyMutex,但它不是普通互斥锁——cpython/lock.h 说明它只占一个字节,用最低两位编码四种状态:

_bits含义
0b00未加锁
0b01已加锁
0b10未加锁,但有线程在等待(parked)
0b11已加锁,且有线程在等待

_PyMutex_Lock 先做一次 CAS,失败才落到慢路径 PyMutex_Lock 把线程 park 起来——「无竞争时零系统调用」。

但「每对象一把锁」会带来 GIL 时代不存在的死锁:Python 操作会嵌套,多线程若按不同顺序拿锁就会互锁。cpython/critical_section.h 给出的解法是临界区(critical section):它是加在 per-object lock 之上的「死锁规避层」,允许线程在嵌套操作时挂起外层锁,且只在真会阻塞时才挂起(减少加解锁次数),I/O 等阻塞操作前后也会挂起锁。头文件里那句注释点破了本质——critical section 与 per-object lock 一起,替代了 GIL 为 dict 等对象提供的线程安全。

11.2.7 嵌入时的 GIL 与宿主线程协作

嵌入场景里,宿主进程往往自带线程(UI 线程、网络线程)。此时 CPython 的 GIL 与宿主线程模型如何协作,是必须想清楚的一环:

  • 谁持有 GIL:Py_Initialize 之后,调用它的那个宿主线程成为解释器主线程并持有 GIL。宿主的其他线程若要调用 Python API,必须先 PyGILState_Ensure() 取得 GIL,用完 PyGILState_Release() 归还。
  • 长计算要主动让出:一段纯 C 长循环若不释放 GIL,会阻塞所有 Python 线程;嵌入方应在循环里周期性用 Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS 释放再取回。
  • 子解释器:3.12+ 支持 per-interpreter GIL(PEP 684),3.14 的 concurrent.interpreters(PEP 734)把它带到标准库层——这与 5.2 里实测的「多子解释器真并行」是同一套机制。

(上述 API 本机未单独编译验证,只讲接口契约。)嵌入时把「谁在什么时候持有 GIL」想清楚,比记 API 名字更重要。

11.2.8 能实测与不能实测的边界

维度标准构建(本机 3.14.6)自由线程 t 构建
Py_GIL_DISABLED01
sys._is_gil_enabled()True默认 False,可用 PYTHON_GIL=1 开回
PyObject 布局ob_refcnt/ob_overflow/ob_flags unionob_tid + PyMutex + 两套 refcount
嵌入开关—PyConfig.enable_gil(仅 #ifdef Py_GIL_DISABLED 下存在)

对嵌入方来说,最后一行最实际:控制 GIL 的字段 enable_gil 只编译进 t 构建,标准构建的头文件里根本没有它。也就是说,能不能在嵌入时开关 GIL,取决于你链接的是哪套 ABI——这又回到 11.1 讲的 abi 标签问题。

本节无法在本机跑自由线程解释器(需从源码 ./configure --disable-gil 构建),所有 t 构建结论均来自 3.14.6 的真实头文件,运行期行为未经本机验证。

小结

  1. 嵌入 CPython 的最小骨架是 Py_Initialize → PyRun_SimpleString → Py_FinalizeEx;PyRun_SimpleString 用 0/-1 报告成败,异常不终止宿主。本机真编译真跑通过。
  2. 现代嵌入用 Py_InitializeFromConfig + PyConfig,参数可控、失败返回 PyStatus;config.optimization_level 与运行期 Py_OptimizeFlag(3.12 起 deprecated)对应同一状态。
  3. 从 C 取回结果:PyImport_AddModule("__main__") → PyModule_GetDict → PyDict_GetItemString(借引用)→ PyLong_AsLong,实测 sum(i*i for i in range(10)) 得 285。
  4. 3.14.6 的 object.h 里 PyObject 有两套布局:标准构建是 ob_refcnt/ob_overflow/ob_flags 的 union;t 构建多出 ob_tid、PyMutex、ob_gc_bits 与两套 refcount——对象头明显变大。
  5. 无 GIL 下的引用计数靠偏向计数:属主线程改 ob_ref_local(快路径),其他线程原子改 ob_ref_shared(慢路径),低两位编码 _Py_REF_QUEUED / _Py_REF_MERGED 等状态。
  6. PyMutex 只占 1 字节、两位编码四态;临界区在其上做死锁规避,替代 GIL 给 dict 等的线程安全。
  7. 本机为标准构建,t 构建结论全部来自真实头文件;控制 GIL 的 PyConfig.enable_gil 只在 t 构建中编译进来。

从运行时回到生态,下一节 11.3 PEP 流程与版本迁移策略 讲这些变化是怎么被提案、被弃用、被迁移进你的项目的。

阅读导航:上一节:11.1 打包与分发机制 · 下一节:11.3 PEP 流程与版本迁移策略 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.1 打包与分发机制