CPython `_thread` 底层线程 API 全解析:函数、锁机制与实现原理
CPython_thread底层线程 API 全解析函数、锁机制与实现原理【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython导读_thread是 CPython 提供给应用程序的最低层线程接口它直接面向轻量级进程式light-weight processes的多线程控制流只提供线程创建与最基础的互斥锁mutex / binary semaphore是上层 threading 模块以及threading.Thread、threading.Lock的 C 语言实现地基。读完本文你将能准确使用start_new_thread()、allocate_lock()等函数与LockType锁方法理解线程标识、栈大小、超时上限等边界行为并通过源码级分析弄清中断、异常上报与信号处理背后的真实机制从而在需要细粒度控制线程且不便引入高层抽象时写出正确、可预期的多线程代码。_thread模块的权威语言参考见 Doc/library/_thread.rst本文所有行为描述均以该文档为骨架并结合仓库中 C 实现与单元测试予以印证。一、模块定位共享数据空间中的多条控制流_thread提供的是底层原语多个线程文档中称之为 :dfn:轻量级进程或任务共享同一个全局数据空间各自独立执行。为了在多线程间协调访问共享数据模块提供简单的锁即文档中所说的 :dfn:互斥锁mutex或二值信号量binary semaphore——同一时刻只有一个线程能够持有它这正是锁存在的全部意义。而日常开发中我们更常用的 threading 模块 是在本模块之上构建的、更易用且更高级的线程 API。换言之需要线程对象、join()、条件变量、队列、线程局部存储等能力 → 用threading只需要开一个线程跑一个函数 一把锁做同步或需要在极底层排查问题时 → 直接面对_thread。从 CPython 源码看这种地基关系十分直观Lib/threading.py 顶部大量引用本模块的实现符号例如_allocate_lock _thread.allocate_lock、_LockType _thread.LockType、get_ident _thread.get_ident、_thread_shutdown _thread._shutdown等threading.Lock实际就是_thread.LockType的封装。可以说理解_thread就等于理解threading的底层心脏。版本与可用性3.7_thread从可选模块变为始终可用always available。此前在未启用线程支持的构建中可能无法导入。模块在 Modules/_threadmodule.c 中实现导出到解释器内建的 C 模块集合中。二、模块定义的异常与常用常量/函数异常error_thread.error在线程相关的特定错误时抛出。import _thread try: _thread.stack_size(123) # 过小见下文 except _thread.error: ...3.3 起error已成为内置RuntimeError的同义词在源码中即ThreadError指向RuntimeError因此捕获RuntimeError也能兜住线程错误。函数start_new_thread(function, args[, kwargs])启动一个新线程并返回其标识identifier。新线程以参数列表args必须是 tuple调用函数function可选参数kwargs是关键字参数字典。import _thread import time def worker(name, delay1.0): time.sleep(delay) print(fworker {name} done) tid _thread.start_new_thread(worker, (A,), {delay: 0.5}) print(started thread id:, tid) time.sleep(1)关键行为文档 源码双重确认函数返回后线程静默退出返回值被忽略。函数抛出未处理异常时由sys.unraisablehook负责处理钩子参数ExceptHookArgs的object属性即传入的函数默认行为是打印堆栈回溯后线程退出其他线程不受影响、继续运行。函数抛出SystemExit时被静默忽略。对应的 C 实现可以精确地看到这一点。在 Modules/_threadmodule.c#L388-L400 的thread_run()中新线程执行PyObject_Call(boot-func, boot-args, boot-kwargs)调用目标函数若返回NULL即发生了异常判断是否匹配PyExc_SystemExit——匹配则PyErr_Clear()静默清除对应SystemExit 被静默忽略其余异常走PyErr_FormatUnraisable()把Exception ignored in thread started by …这类消息交给异常上报机制对应sys.unraisablehook处理。此外Modules/_threadmodule.c#L1935-L1952 对参数做了严格的类型检查第一参数必须是可调用对象否则抛TypeError: first arg must be callable第二参数必须是 tuple否则抛TypeError: 2nd arg must be a tuple可选第三参数必须是 dict否则抛TypeError: optional 3rd arg must be a dictionary。审计事件audit eventstart_new_thread会在启动前触发审计事件_thread.start_new_thread参数为function, args, kwargs见 Modules/_threadmodule.c#L1954-L1957 的PySys_Audit(_thread.start_new_thread, OOO, ...)。这意味着可以通过 PEP 578 审计钩子监控程序中所有新线程的创建。边界与限制源码层在 Modules/_threadmodule.c#L1896-L1927 的do_start_new_thread()中还隐含两条运行时约束隔离子解释器未开启线程特性时创建线程会抛RuntimeError: thread is not supported for isolated subinterpreters解释器正在结束finalizing时抛PythonFinalizationError: cant create new thread at interpreter shutdown。start_new_thread()返回的标识与 get_ident() 获取的当前线程标识语义一致返回值的细节见下文。值得一提的还有两个源码内保留的过时同义词_thread.start_new与start_new_thread()等价见 Modules/_threadmodule.c#L1988-L1992但文档不推荐第三方代码使用它们。函数interrupt_main(signumsignal.SIGINT, /)模拟一个信号到达主线程的效果子线程可用它去中断主线程但并不保证中断会立即发生。import _thread import time def wake_up_main(): time.sleep(1.0) print(worker interrupts main thread now) _thread.interrupt_main() # 等价于模拟 SIGINT _thread.start_new_thread(wake_up_main, ()) try: time.sleep(5) except KeyboardInterrupt: print(main received KeyboardInterrupt)参数与细节signum为要模拟的信号编号缺省时模拟signal.SIGINT即通常让主线程在等待点收到KeyboardInterrupt。若给定信号未被 Python 处理被设为signal.SIG_DFL或signal.SIG_IGN该函数什么也不做。3.10起新增signum参数用于定制信号编号。实现上Modules/_threadmodule.c#L2086-L2099 中的thread_PyThread_interrupt_main()直接调用PyErr_SetInterruptEx(signum)信号编号越界则抛ValueError。⚠️ 特别注意文档中的注释该函数并不真正发射信号而是调度对关联处理器如果存在的一次调用。如果确实要真正发出信号请使用 signal.raise_signal()。换句话说interrupt_main影响的是 Python 层面的中断/处理器调度而非操作系统层面的信号递送。函数exit()抛出SystemExit异常。若未被捕获将导致线程静默退出。文档同时说明调用sys.exit()或抛出SystemExit与调用_thread.exit()等价参见下文Caveats。def short_lived(): _thread.exit() # 立即静默终止当前线程其 C 实现 Modules/_threadmodule.c#L2066-L2069 就是PyErr_SetNone(PyExc_SystemExit)而源码内保留的过时同义词_thread.exit_thread()与其完全等价Modules/_threadmodule.c#L2080-L2084。注意这是退出当前线程与退出整个进程不同文档注释中提及的旧exit_prog(status)概念退出所有线程并把整数status作为整个程序退出码且不执行本线程或其他线程挂起的finally子句并未在当前公开 API 中暴露。标识函数get_ident()/get_native_id()get_ident()返回当前线程的线程标识符一个非零整数其值没有直接意义只应作为魔法 cookie使用例如作为字典中线程专属数据的索引线程退出后、另一线程被创建时标识可能被回收重用因此不能把历史标识当作长期唯一值。import _thread def show_ident(): print(inside thread:, _thread.get_ident()) tid _thread.start_new_thread(show_ident, ()) print(returned tid :, tid)C 实现 Modules/_threadmodule.c#L2133-L2142 通过PyThread_get_thread_ident_ex()获取并返回无符号长整型PyLong_FromUnsignedLongLong。threading层的threading.get_ident()与本函数完全同源见 Lib/threading.py 中的get_ident _thread.get_ident这也是为什么两个get_ident的返回完全可比对。get_native_id()返回内核为当前线程分配的原生整型线程 IDnative integral Thread ID一个非负整数可用于在系统范围内唯一标识该特定线程直到线程终止此后 OS 可能回收该值与get_ident()不同它反映的是操作系统内核视角的真实线程身份常用于对接外部调试器、性能剖析工具或操作系统 API。import _thread print(kernel TID:, _thread.get_native_id())可用性Windows、FreeBSD、Linux、macOS、OpenBSD、NetBSD、AIX、DragonFlyBSD、GNU/kFreeBSD、Solaris。3.8新增3.13起支持 GNU/kFreeBSD3.15起支持 Solaris。在 Modules/_threadmodule.c#L2156-L2171 中该函数被#ifdef PY_HAVE_THREAD_NATIVE_ID保护即在不支持原生 ID 的平台构建上该函数不存在。注意它返回unsigned long语义上的非负整数而threading中线程对象的.native_id属性即取自这里。函数stack_size([size])返回创建新线程时使用的线程栈大小传入可选的size则设置之后创建线程所用的栈大小。import _thread print(default stack:, _thread.stack_size()) # 平台默认通常为 0表示使用默认 _thread.stack_size(262144) # 设为 256 KiB print(now set to :, _thread.stack_size()) _thread.stack_size(0) # 恢复平台默认规则与边界size必须是0使用平台或配置默认值或至少 3276832 KiB的正整数不传size时按0处理若平台不支持修改线程栈大小抛RuntimeError即_thread.error若指定的大小无效过小、为负、不满足平台限制等抛ValueError且栈大小保持不变32 KiB 是当前支持的最小栈大小用于保证解释器自身有足够的栈空间某些平台对栈大小有额外限制例如要求最小值大于 32 KiB或要求按系统内存页大小的整数倍分配。4 KiB 页很常见在没有更具体信息时建议使用 4096 的倍数作为栈大小取值。C 实现 Modules/_threadmodule.c#L2192-L2225 给出了精确的最小值判定new_size非 0 且小于_PyOS_MIN_STACK_SIZE SYSTEM_PAGE_SIZE时报ValueError底层PyThread_set_stacksize()返回-1大小无效时报ValueError返回-2不支持设置时走ThreadError即RuntimeError分支成功后返回旧的栈大小。测试 Lib/test/test_thread.py#L72-L116 对这一行为做了覆盖断言初始栈大小为 0stack_size(123)过小抛ValueErrorstack_size(-4096)负数抛ValueError而262144、0x100000、0均可设置并能读回随后用这些栈大小实际创建线程验证可运行。可用性Windows、以及 POSIX 线程pthreads平台。常量TIMEOUT_MAX锁方法Lock.acquire(timeout...)即threading.Lock.acquire的timeout参数允许的最大值以秒为单位。指定超过该值的超时会抛OverflowError。import _thread, threading lock threading.Lock() # 内部即 _thread.LockType try: lock.acquire(timeout_thread.TIMEOUT_MAX 1) except OverflowError: print(timeout too large)3.2起加入。在 Modules/_threadmodule.c#L2766-L2773 处可见其来源模块初始化时用PY_TIMEOUT_MAX以微秒计的 C 层上限换算为秒TIMEOUT_MAX (double)PY_TIMEOUT_MAX * 1e-6后写入模块命名空间。对应地C 层的超时解析在 Modules/_threadmodule.c#L780-L815 的lock_acquire_parse_timeout()中当换算出的微秒数大于PY_TIMEOUT_MAX时抛OverflowError: timeout value is too large。此外timeout为负数时报ValueError: timeout value must be a non-negative number未设置-1除外blockingFalse时若指定timeout会报ValueError: cant specify a timeout for a non-blocking call。三、锁对象LockType互斥与同步的基础allocate_lock()与类型LockTypeimport _thread a_lock _thread.allocate_lock() # 返回新的锁对象初始为未锁定allocate_lock()返回一个新锁对象初始处于未锁定状态LockType是锁对象的类型。文档约定锁对象的方法名基于LockType描述其方法与 threading.Lock 一致底层是同一实现源码保留了过时同义词_thread.allocate等价于allocate_lock()见 Modules/_threadmodule.c#L2127-L2131锁方法acquire_lock/release_lock/locked_lock分别是acquire/release/locked的过时别名。锁对象提供以下方法acquire(blockingTrue, timeout-1)无条件获取不传可选参数时若锁已被其他线程持有则一直等待直到其被释放。同一时刻只能有一个线程持有锁。blockingFalse仅当可以立即取得锁时才获取不等待blockingTrue与无条件获取行为相同timeout浮点数正数指定最多等待的秒数负timeout表示无限等待blockingFalse时不能同时指定timeout返回值成功获取返回True否则返回False。import _thread lock _thread.allocate_lock() # 无条件获取会阻塞直到成功 lock.acquire() try: # 临界区…… pass finally: lock.release() # 非阻塞探测 if lock.acquire(False): try: pass # 拿到了锁 finally: lock.release() else: print(锁正被占用) # 带超时的等待 ok lock.acquire(timeout2.0) print(acquired within 2s:, ok)版本演进文档明确记录均与锁底层可被信号中断相关3.2timeout参数新增3.2POSIX 上锁的获取现在可被信号中断3.14Windows 上锁的获取现在也可被信号中断。这与底层实现密切相关锁获取最终落到 Modules/_threadmodule.c#L831-L854 的_thread_lock_acquire_impl()它调用_PyMutex_LockTimed()并显式传入_PY_LOCK_HANDLE_SIGNALS标志——这正是阻塞获取过程可以被信号打断、从而能在主线程响应KeyboardInterrupt的实现来源。release()释放锁。锁必须已处于被获取状态但不要求由获取它的同一个线程来释放——这是原始互斥锁非可重入锁与RLock的重要区别之一。C 层 docstring 明确写着 The lock must be in the locked state, but it neednt be locked by the same thread that unlocks it见 Modules/_threadmodule.c#L870-L879 附近。locked()返回锁当前状态若已被某线程获取返回True否则返回False。常用于非阻塞地检查互斥量状态实现见 Modules/_threadmodule.c#L945-L961 一带。支持with语句除上述方法外锁对象还可用于with语句保证异常路径下也会正确释放import _thread a_lock _thread.allocate_lock() with a_lock: print(a_lock is locked while this executes)with块等价于acquire()/release()的成对调用是推荐的临界区写法。一个完整的线程同步示例import _thread import time counter 0 counter_lock _thread.allocate_lock() def increment(times10000): global counter for _ in range(times): with counter_lock: # 每次自增都持锁 counter 1 threads [] for _ in range(4): tid _thread.start_new_thread(increment, ()) threads.append(tid) time.sleep(1.0) # 等待工作线程结束生产代码应使用更可靠的同步 print(final counter:, counter) # 应为 40000这个例子的要点是所有线程共享全局counter若不使用counter_lock保护字节码级的读-改-写会被交错执行而导致计数丢失有了_thread提供的互斥锁临界区得以串行化。四、Caveats使用_thread必须牢记的三条边界文档在结尾明确列出使用本模块时的注意点理解它们有助于避免写出看起来对、跑起来错的线程代码中断总是流向主线程KeyboardInterrupt异常只会被主线程接收到。子线程无法自己收到来自终端的中断——如果需要子线程触发主线程的中断应使用interrupt_main()。sys.exit/SystemExit与_thread.exit等价在线程函数内调用sys.exit()或抛出SystemExit效果都是调用_thread.exit()即静默终止当前线程而不会像主线程中那样触发进程退出流程。主线程退出时子线程的命运由系统定义在大多数系统上主线程退出时其他线程会被直接杀死且不会执行try...finally子句也不会执行对象析构。因此若需要清理资源必须在主线程退出前显式等待所有工作线程结束例如通过锁/事件协调或直接改用threading的join()。五、源码视角这些行为在仓库中如何落地模块实现Modules/_threadmodule.c 是_thread的完整 C 实现约 2800 行线程的引导、执行与收尾围绕thread_run()展开子线程先PyEval_AcquireThread()获取 GIL再调用目标函数结束后释放 GIL、清理线程状态并递减解释器内的线程计数interp-threads.count。它同时解释了SystemExit静默、unraisable上报、解释器结束时的线程终止策略等文档行为。内部辅助符号模块还向threading暴露了一批内部接口——例如_count()返回当前正在运行的 Python 线程数不含主线程、_excepthook与_ExceptHookArgs用于打印线程未捕获异常的回溯对应 Modules/_threadmodule.c#L2305-L2381 的实现。这些仅供内部使用文档中不推荐第三方调用threading.enumerate()才是常规选择。高层封装的对应物Lib/threading.py 中大量符号直接绑定到本模块threading.Thread、threading.Lock、threading.get_ident()、threading.TIMEOUT_MAX、threading.stack_size()最终都落回_thread。因此凡是文档中有关Lock.acquire/timeout的约束如TIMEOUT_MAX、OverflowError对threading.Lock同样成立。单元测试Lib/test/test_thread.py 覆盖了线程创建test_starting_threads创建NUMTASKS个线程并用一把完成锁等待全部退出、栈大小test_stack_size、test_nt_and_posix_stack_size等核心行为共享的 Lib/test/test_lock.py 系列则从test.lock_tests验证锁的语义。想要深入源码学习的读者可以以这两个文件为起点。六、何时该用_thread何时该用threading优先选择threading绝大多数应用场景线程对象管理、join()、daemon线程、RLock、条件变量、队列、线程池、异常回调等都应使用 threading 模块。它在_thread之上封装了线程生命周期与丰富的同步工具能显著降低出错概率。_thread的适用场景需要极小的运行时开销、明确只需要原始线程 原始互斥锁、或需要在 C 扩展/嵌入场景中对接最底层线程语义时此外在研读threading源码、排查 GIL 与线程调度问题时_thread是实现层的事实依据。可移植性提醒_thread自 3.7 起总是可用但部分功能get_native_id、stack_size的设置能力等受平台支持限制具体以 上文各函数说明 标注的可用性为准跨平台代码应做好降级处理。总而言之_thread是 CPython 多线程世界的原子零件库它只承诺两件事——开线程、给锁但把这两件事做到最贴近操作系统原语理解它是理解 Python 线程模型与 GIL 协作机制的最佳切入点。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考