python-subprocess-and-concurrency
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePython Subprocess & Concurrency
Python Subprocess 与并发
Two topics that produce the same class of bug — a program that passes every smoke test and then hangs, leaks children, or silently drops failures under load. 29 rules in 3 categories.
A rule match is a signal, not a verdict. Concurrency advice is context-dependent: a fixed handful of tasks needs no ceremony, and a pool is still fine in a single-threaded program. Check the rule's scope line before flagging or refactoring.
fork这两个主题会引发同一类问题:程序通过所有 smoke test,却会在负载下卡死、泄漏子进程,或悄无声息地丢失失败结果。共 29 条规则,分为 3 个类别。
规则匹配只是一个信号,不是最终结论。并发建议取决于具体上下文:固定数量的少量任务无需任何复杂机制,而 池在单线程程序中仍然可以使用。在标记或重构之前,请先检查规则的 scope 行。
forkScope: what this skill owns, and what it defers
Scope:本 skill 负责什么,以及哪些内容交由其他 skill 处理
| Topic | Owner |
|---|---|
| this skill ( |
| this skill ( |
| this skill ( |
| |
| The offload decision itself: GIL vs threads vs process pool, default executor sizing, thread non-interruptibility | |
| General Python style, typing, error handling, dataclasses | |
| Test determinism and synchronization | |
Do not restate the deferred skills' rules here. Where they overlap, this skill states the same principle in the process/thread domain and names their rule (for example for bounded fan-out, for concurrency tests).
async-bound-concurrencydeterminism-sync-not-sleepRead the ownership honestly before claiming a gap. already covers the offload decision, the then executor-sizing formula, free-threaded 3.13+ builds, and the fact that a running thread cannot be interrupted. , and go deeper on those for code that never touches asyncio, and each names that rule as the earlier statement rather than claiming the ground.
async-no-blocking-event-loopmin(32, cpu + 4)process_cpu_count()thread-for-io-not-cputhread-size-and-boundthread-cannot-be-killedWhat is genuinely uncovered upstream, and owned here: in full; thread ownership, join and ; lock discipline and pool deadlocks; exit semantics; multiprocessing start methods, pickling, IPC and pool failure; and hang, orphan and zombie diagnosis.
subprocessexcepthookThreadPoolExecutor| 主题 | 负责方 |
|---|---|
| 本 skill( |
| 本 skill( |
| 本 skill( |
| |
| 卸载决策本身:GIL 与线程和进程池的选择、默认 executor 大小、线程无法中断 | |
| 通用 Python 风格、类型标注、错误处理、dataclass | |
| 测试确定性与同步 | |
不要在这里重复其他 skill 负责的规则。发生重叠时,本 skill 会说明进程和线程领域中的*相同原则,并指明相应规则(例如,有限 fan-out 对应 ,并发测试对应 )。
async-bound-concurrencydeterminism-sync-not-sleep在声称存在缺口之前,应诚实判断职责边界。 已经涵盖卸载决策、 后使用 的 executor 大小公式、free-threaded 3.13+ 构建,以及运行中的线程无法被中断这一事实。对于完全不涉及 asyncio 的代码,、 和 会对这些问题进行更深入的说明;每条规则都将相关规则作为先前的依据,而不会重复宣称其基础。
async-no-blocking-event-loopmin(32, cpu + 4)process_cpu_count()thread-for-io-not-cputhread-size-and-boundthread-cannot-be-killed上游确实尚未涵盖、且由本 skill 负责的内容包括:完整的 ;线程所有权、join 和 ;锁规范与池死锁; 的退出语义;multiprocessing 启动方式、pickle、IPC 和进程池故障;以及卡死、孤儿进程和僵尸进程的诊断。
subprocessexcepthookThreadPoolExecutorPick the model before writing code
编写代码前先选择模型
The most expensive concurrency mistake is choosing a model out of habit. Choose by what the work waits on and whether it can run in parallel.
| The work | Model | Why |
|---|---|---|
| Nothing that can overlap | plain loop | Concurrency is pure cost: workers and failure modes for zero gain |
| Many blocking I/O calls on async-capable libraries | | See |
| Blocking I/O with libraries that have no async equivalent | | Simplest tool that overlaps the wait |
| Pure-Python CPU work | | The GIL serializes bytecode; processes do not share it |
| CPU work inside a C extension that releases the GIL | threads | The GIL is not the limit for that call |
CPU work, free-threaded build ( | threads | No GIL to serialize them (PEP 703/779); measure, and confirm C extensions did not re-enable it |
| Thousands of small CPU tasks with little shared state | | Each interpreter has its own GIL: multicore parallelism without process startup |
| An external program, script, or CLI | | It is a separate process whether you like it or not |
Sizing facts worth knowing (verified against the 3.14 standard-library docs):
- (3.13+) is the count usable by this process — it respects CPU affinity and container limits.
os.process_cpu_count()is machine-wide and can returnos.cpu_count(). Use the former for pool sizing.None - defaults to
ThreadPoolExecutor(max_workers=None).min(32, (os.process_cpu_count() or 1) + 4) - defaults to
ProcessPoolExecutor(max_workers=None), capped at 61 on Windows.os.process_cpu_count() - is a
InterpreterPoolExecutorsubclass: one worker thread each, one interpreter each. Interpreters cannot share a mutable object, so it forces an explicit data contract.ThreadPoolExecutor
代价最高的并发错误,是出于习惯选择并发模型。应根据任务在等待什么,以及任务能否并行执行来决定。
| 工作类型 | 模型 | 原因 |
|---|---|---|
| 没有任何可以重叠执行的任务 | 普通循环 | 并发只有成本:worker 和故障模式不会带来任何收益 |
| 在支持 async 的库上执行大量阻塞 I/O 调用 | | 参见 |
| 库没有 async 等价实现时的阻塞 I/O | | 重叠等待时间最简单的工具 |
| 纯 Python CPU 工作 | | GIL 会串行化字节码;进程不会共享 GIL |
| 会释放 GIL 的 C extension 内部 CPU 工作 | 线程 | 该调用不受 GIL 限制 |
CPU 工作,free-threaded 构建( | 线程 | 没有 GIL 将其串行化(PEP 703/779);需要进行测量,并确认 C extension 没有重新启用 GIL |
| 数千个共享状态很少的小型 CPU 任务 | | 每个解释器都有自己的 GIL:无需进程启动开销即可实现多核并行 |
| 外部程序、脚本或 CLI | | 无论是否愿意,它都是一个独立进程 |
值得了解的大小计算事实(已根据 3.14 标准库文档验证):
- (3.13+)返回当前进程可用的 CPU 数量——它会考虑 CPU affinity 和容器限制。
os.process_cpu_count()返回整台机器的 CPU 数量,并且可能返回os.cpu_count()。应使用前者确定进程池大小。None - 默认使用
ThreadPoolExecutor(max_workers=None)。min(32, (os.process_cpu_count() or 1) + 4) - 默认使用
ProcessPoolExecutor(max_workers=None),在 Windows 上上限为 61。os.process_cpu_count() - 是
InterpreterPoolExecutor的子类:每个 worker 线程对应一个解释器。解释器之间无法共享可变对象,因此必须明确设计数据契约。ThreadPoolExecutor
Quick reference
快速参考
Subprocess (sp-
)
sp-Subprocess(sp-
)
sp-- — Pass an argv sequence; never interpolate values into
sp-argv-not-shellshell=True - —
sp-check-and-capture(or readcheck=Trueexplicitly);returncodedefaults tocheckFalse - — Every wait has a timeout;
sp-timeout-alwayskills only the direct childrun(timeout=) - —
sp-kill-the-process-tree+start_new_session=Trueto actually stop descendantsos.killpg - — Use
sp-communicate-not-waitwith pipes;communicate()/.wait()deadlocks, and a surviving descendant keeps EOF from arriving.read() - —
sp-popen-only-when-neededby default;run()waits, it does not killwith Popen - —
sp-close-stdinfor non-interactive childrenstdin=DEVNULL - — Full executable path, explicit
sp-explicit-env-and-cwdandcwdenv - —
sp-explicit-text-encoding, never the locale by accidentencoding="utf-8" - —
sp-no-preexec-fnis thread-unsafe; usepreexec_fn/start_new_sessionprocess_group - — One child per input item is unbounded fan-out
sp-bound-child-fanout - — Not
sp-drop-legacy-apis,os.system,os.popen,os.spawn*getstatusoutput - — In async code use
sp-async-children+create_subprocess_execasyncio.timeout
- — 传入 argv 序列;绝不要将值插入
sp-argv-not-shellshell=True - — 使用
sp-check-and-capture(或显式读取check=True);returncode默认为checkFalse - — 每次等待都必须设置 timeout;
sp-timeout-always只会终止直接子进程run(timeout=) - — 使用
sp-kill-the-process-tree+start_new_session=True,才能真正停止所有后代进程os.killpg - — 带管道时使用
sp-communicate-not-wait;communicate()/.wait()会导致死锁,而残留的后代进程会阻止 EOF 到达.read() - — 默认使用
sp-popen-only-when-needed;使用run()时只会等待,不会终止进程with Popen - — 对非交互式子进程使用
sp-close-stdinstdin=DEVNULL - — 使用完整的 executable 路径,并显式指定
sp-explicit-env-and-cwd和cwdenv - — 使用
sp-explicit-text-encoding,不要意外采用 locale 编码encoding="utf-8" - —
sp-no-preexec-fn不具备线程安全性;应使用preexec_fn/start_new_sessionprocess_group - — 为每个输入项创建一个子进程会造成无限 fan-out
sp-bound-child-fanout - — 不使用
sp-drop-legacy-apis、os.system、os.popen、os.spawn*getstatusoutput - — 在 async 代码中使用
sp-async-children+create_subprocess_execasyncio.timeout
Threads (thread-
)
thread-线程(thread-
)
thread-- — No stop/suspend; stop cooperatively, and not with daemon threads
thread-cannot-be-killed - — Join every thread or use an executor; a failed thread prints and vanishes
thread-own-and-join - — Set
thread-size-and-bound; bound submission the waymax_workersbounds fan-outasync-bound-concurrency - — A worker waiting on another future deadlocks the pool
thread-never-wait-on-a-future - — Threads do not speed up pure-Python CPU work
thread-for-io-not-cpu - — Pass data, guard shared mutable state, keep locks short and ordered
thread-share-by-message - — The exit handler joins threads before
thread-no-pool-for-long-running-work; a pool is not a scheduleratexit - — Context manager or explicit
thread-explicit-shutdown;shutdown()only reaches queued workcancel_futures
- — 无法停止或挂起线程;应采用协作式停止,而且不要使用 daemon thread
thread-cannot-be-killed - — Join 每个线程,或者使用 executor;线程失败后会打印错误并消失
thread-own-and-join - — 设置
thread-size-and-bound;像max_workers限制 fan-out 一样限制任务提交量async-bound-concurrency - — worker 等待另一个 future 会导致进程池死锁
thread-never-wait-on-a-future - — 线程无法加速纯 Python CPU 工作
thread-for-io-not-cpu - — 传递数据,保护共享可变状态,锁应保持简短且获取顺序固定
thread-share-by-message - — 退出处理会在
thread-no-pool-for-long-running-work前 join 线程;进程池不是调度器atexit - — 使用 context manager 或显式调用
thread-explicit-shutdown;shutdown()只能取消排队中的工作cancel_futures
Processes (proc-
)
proc-进程(proc-
)
proc-- — Name the start method;
proc-choose-start-methodis no longer a default anywherefork - —
proc-main-guardplusif __name__ == "__main__":freeze_support() - — Workers are module-level functions; args and results are pickled
proc-top-level-and-picklable - — Set up each worker in
proc-inherit-dont-ship; inherit resources instead of shipping theminitializer - — Queues for data,
proc-message-passfor one-to-one, proxies sparinglyPipe - — A killed worker breaks the whole pool; catch
proc-expect-a-broken-poolBrokenExecutor - — Terminating a worker corrupts shared queues; signals are main-thread-only
proc-stop-cooperatively - — Size the pool yourself;
proc-bound-and-chunkmatters,chunksizecollects eagerlymap
- — 明确指定启动方式;
proc-choose-start-method已不再是任何平台的默认方式fork - — 使用
proc-main-guard,并调用if __name__ == "__main__":freeze_support() - — Worker 必须是模块级函数;参数和结果会被 pickle
proc-top-level-and-picklable - — 在
proc-inherit-dont-ship中设置每个 worker;应继承资源,而不是传输资源initializer - — 使用 Queue 传递数据,使用
proc-message-pass进行一对一通信,谨慎使用 proxyPipe - — worker 被终止会破坏整个进程池;应捕获
proc-expect-a-broken-poolBrokenExecutor - — 强制终止 worker 会破坏共享 Queue;信号只能在主线程中处理
proc-stop-cooperatively - — 自行确定进程池大小;
proc-bound-and-chunk很重要,chunksize会立即收集结果map
The patterns that carry most of the weight
最关键的模式
Every external command, in one shape. Argv, closed stdin, explicit encoding, timeout, checked exit code:
python
import subprocess
def git_rev(path: str) -> str:
result = subprocess.run(
["/usr/bin/git", "-C", path, "rev-parse", "HEAD"],
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=30,
check=True,
)
return result.stdout.strip()A timeout that stops the whole tree. kills only the direct child; anything it spawned keeps running and keeps holding your resources.
run(timeout=)python
import os
import signal
import subprocess
def run_tree(argv: list[str], *, timeout: float, grace: float = 5.0) -> int:
# start_new_session=True puts the child in its own session, so its process
# group id equals its pid: one killpg stops the child and every descendant.
proc = subprocess.Popen(argv, start_new_session=True, stdin=subprocess.DEVNULL)
try:
return proc.wait(timeout=timeout)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGTERM) # ask the whole group to stop
try:
proc.wait(timeout=grace) # give it a moment to exit cleanly
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL) # then stop asking
proc.wait() # always reap
raiseBounded child fan-out. One child per input item is a fork bomb dressed as a loop:
python
from concurrent.futures import ThreadPoolExecutor
import subprocess
def run_all(argv_list: list[list[str]], *, max_parallel: int = 8) -> list[str]:
def one(argv: list[str]) -> str:
done = subprocess.run(
argv, capture_output=True, text=True, encoding="utf-8",
timeout=60, check=True,
)
return done.stdout
# max_workers bounds how many children exist at once. Same principle as
# async-bound-concurrency, applied to processes instead of coroutines.
with ThreadPoolExecutor(max_workers=max_parallel) as executor:
return list(executor.map(one, argv_list))Shutdown you can predict. A daemon thread is killed mid-work at interpreter exit; a worker pool joins what it accepted.
python
from concurrent.futures import ThreadPoolExecutor
def handle(item: str) -> None: ...
with ThreadPoolExecutor(max_workers=4) as executor: # shutdown(wait=True) on exit
futures = [executor.submit(handle, item) for item in items]
for future in futures:
future.result() # never let a worker's exception disappearPer-worker setup, results as plain data. Both executors give you a one-time per-worker hook; use it instead of reopening a connection per task, and return data instead of shipping live objects:
python
import multiprocessing
from concurrent.futures import ProcessPoolExecutor
_conn = None # assigned once per worker by init_worker()
def init_worker(dsn: str) -> None:
global _conn
_conn = connect(dsn) # opened once per worker process, not once per task
def score(row_id: int) -> float:
return _conn.query(row_id) # must be importable at module top level to be picklable
if __name__ == "__main__": # required by the spawn and forkserver start methods
ctx = multiprocessing.get_context("spawn") # name it; never inherit the default blindly
with ProcessPoolExecutor(max_workers=8, mp_context=ctx,
initializer=init_worker, initargs=(dsn,)) as executor:
results = list(executor.map(score, row_ids, chunksize=200))所有外部命令都采用同一种形式。 使用 argv、关闭 stdin、显式指定 encoding、设置 timeout,并检查退出码:
python
import subprocess
def git_rev(path: str) -> str:
result = subprocess.run(
["/usr/bin/git", "-C", path, "rev-parse", "HEAD"],
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=30,
check=True,
)
return result.stdout.strip()能够终止整个进程树的 timeout。 只会终止直接子进程;子进程创建的任何程序仍会继续运行,并持续占用资源。
run(timeout=)python
import os
import signal
import subprocess
def run_tree(argv: list[str], *, timeout: float, grace: float = 5.0) -> int:
# start_new_session=True puts the child in its own session, so its process
# group id equals its pid: one killpg stops the child and every descendant.
proc = subprocess.Popen(argv, start_new_session=True, stdin=subprocess.DEVNULL)
try:
return proc.wait(timeout=timeout)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGTERM) # ask the whole group to stop
try:
proc.wait(timeout=grace) # give it a moment to exit cleanly
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL) # then stop asking
proc.wait() # always reap
raise限制子进程 fan-out。 为每个输入项创建一个子进程,就是伪装成循环的 fork bomb:
python
from concurrent.futures import ThreadPoolExecutor
import subprocess
def run_all(argv_list: list[list[str]], *, max_parallel: int = 8) -> list[str]:
def one(argv: list[str]) -> str:
done = subprocess.run(
argv, capture_output=True, text=True, encoding="utf-8",
timeout=60, check=True,
)
return done.stdout
# max_workers bounds how many children exist at once. Same principle as
# async-bound-concurrency, applied to processes instead of coroutines.
with ThreadPoolExecutor(max_workers=max_parallel) as executor:
return list(executor.map(one, argv_list))可预测的关闭流程。 daemon thread 会在解释器退出时被中途终止;worker pool 则会等待已接受的任务完成。
python
from concurrent.futures import ThreadPoolExecutor
def handle(item: str) -> None: ...
with ThreadPoolExecutor(max_workers=4) as executor: # shutdown(wait=True) on exit
futures = [executor.submit(handle, item) for item in items]
for future in futures:
future.result() # never let a worker's exception disappear每个 worker 独立初始化,结果使用纯数据。 两种 executor 都提供一次性 per-worker hook;应使用它,而不是为每个任务重新建立连接,并且应返回数据,而不是传输活对象:
python
import multiprocessing
from concurrent.futures import ProcessPoolExecutor
_conn = None # assigned once per worker by init_worker()
def init_worker(dsn: str) -> None:
global _conn
_conn = connect(dsn) # opened once per worker process, not once per task
def score(row_id: int) -> float:
return _conn.query(row_id) # must be importable at module top level to be picklable
if __name__ == "__main__": # required by the spawn and forkserver start methods
ctx = multiprocessing.get_context("spawn") # name it; never inherit the default blindly
with ProcessPoolExecutor(max_workers=8, mp_context=ctx,
initializer=init_worker, initargs=(dsn,)) as executor:
results = list(executor.map(score, row_ids, chunksize=200))Debugging a hang
调试卡死问题
A hang is a missing timeout, a full pipe, or a lock. In that order.
- Get a stack out of a live process: , or run with
faulthandler.dump_traceback_later(60, exit=True). It is implemented in C, so it still reports during a deadlock in Python code.python -X faulthandler - prints every thread's stack without restarting the process;
py-spy dump --pid <pid>shows what is burning CPU;py-spy top --pid <pid>adds frame locals. In containers it needs the--localscapability.SYS_PTRACE - lists thread objects that are still alive;
threading.enumerate()does the same for children.multiprocessing.active_children() - Orphans and zombies point at the opposite mistakes: a child outliving its parent means the wrong thing was killed (), and an unreaped child means nobody joined it (
sp-kill-the-process-tree,thread-own-and-join).proc-main-guard
Commands and diagnosis order: .
references/debugging-hangs.md卡死通常源于缺少 timeout、管道已满或锁。应按此顺序排查。
- 从运行中的进程获取 stack:使用 ,或者通过
faulthandler.dump_traceback_later(60, exit=True)运行。它以 C 实现,因此即使 Python 代码发生死锁,仍能输出报告。python -X faulthandler - 可以在不重启进程的情况下打印每个线程的 stack;
py-spy dump --pid <pid>用于显示 CPU 占用热点;py-spy top --pid <pid>会增加 frame locals。在容器中运行时,它需要--localscapability。SYS_PTRACE - 会列出仍然存活的线程对象;
threading.enumerate()则用于列出仍然存活的子进程。multiprocessing.active_children() - 孤儿进程和僵尸进程指向相反的错误:子进程比父进程活得更久,说明终止了错误的进程();子进程未被回收则说明没有人 join 它(
sp-kill-the-process-tree、thread-own-and-join)。proc-main-guard
命令与诊断顺序参见 。
references/debugging-hangs.mdReview checklist
审查清单
- Every call passes an argv sequence, and
subprocessappears nowhere.shell=True - Every call sets or inspects
check=Truedeliberately.returncode - Every call has a timeout, and a timed-out child cannot leave descendants running or holding its pipes open.
- No ,
.read(), or.stdin.write()against a.wait();PIPEis used.communicate() - is explicit when text mode is used.
encoding - (or explicit
stdin=DEVNULL) for anything non-interactive.input= - Executable path, , and
cwdare explicit where the result must be reproducible.env - No .
preexec_fn - Fan-out is bounded: at most N children or N workers, chosen from a real downstream limit.
- No ,
os.system,os.popen, oros.spawn*.subprocess.getstatusoutput - Every thread is joined, or lives in an executor that is shut down.
- No daemon thread owns work whose interruption would corrupt state.
- No worker waits on another task's future.
- Shared mutable state across threads is message-passed or guarded; locks are acquired in one global order and never held across a blocking call.
- CPU-bound work in threads is justified by a free-threaded build or a GIL-releasing extension.
- Pool sizing is explicit, and uses (3.13+) rather than
os.process_cpu_count().os.cpu_count() - The multiprocessing start method is named explicitly, and is not used in a threaded process.
fork - Worker functions are module-level and picklable; args and results are plain data.
- guards the entry point of anything that spawns processes.
if __name__ == "__main__": - /
BrokenExecutoris handled where a worker can die.BrokenProcessPool - Shutdown is cooperative for threads and processes; is not used on workers holding shared queues.
terminate()
- 每个 调用都传入 argv 序列,并且任何地方都没有
subprocess。shell=True - 每个调用都设置 ,或有意检查
check=True。returncode - 每个调用都设置 timeout,且超时后子进程无法留下仍在运行或占用管道的后代进程。
- 不对 使用
PIPE、.read()或.stdin.write();改用.wait()。communicate() - 使用文本模式时显式指定 。
encoding - 对任何非交互式进程使用 (或显式提供
stdin=DEVNULL)。input= - 当结果必须可复现时,显式指定 executable 路径、和
cwd。env - 不使用 。
preexec_fn - Fan-out 受到限制:最多存在 N 个子进程或 N 个 worker,数量应根据真实的下游限制确定。
- 不使用 、
os.system、os.popen或os.spawn*。subprocess.getstatusoutput - 每个线程都被 join,或位于已关闭的 executor 中。
- 没有 daemon thread 承担一旦被中断就会破坏状态的工作。
- 没有 worker 等待另一个任务的 future。
- 线程之间的共享可变状态通过消息传递或受锁保护;所有位置都按同一个全局顺序获取锁,且绝不跨阻塞调用持有锁。
- 线程中的 CPU 密集型工作只有在 free-threaded 构建或会释放 GIL 的 extension 中才有合理性。
- 明确指定进程池大小,并使用 (3.13+),而不是
os.process_cpu_count()。os.cpu_count() - 明确指定 multiprocessing 启动方式,并且不在多线程进程中使用 。
fork - Worker 函数位于模块级且可被 pickle;参数和结果均为纯数据。
- 任何创建进程的入口点都由 保护。
if __name__ == "__main__": - 在 worker 可能死亡的位置处理 /
BrokenExecutor。BrokenProcessPool - 线程和进程采用协作式关闭;绝不对持有共享 Queue 的 worker 使用 。
terminate()
References
参考资料
- — the
references/subprocess.mdrules with failure modes, code, and platform notes.sp- - — the
references/threads.mdrules: GIL, free-threading, ownership, locks, pool behavior.thread- - — the
references/processes.mdrules: start methods, pickling, IPC, pool lifecycle.proc- - — hang, deadlock, orphan, and zombie diagnosis with commands.
references/debugging-hangs.md
Run to re-check this skill's recipes and version claims on the interpreter you have (repo tooling, wired into ; execute it, do not read it into context). It prints one line per check naming the rule it backs, skips POSIX-only checks off POSIX, and asserts the documented API floor for your Python version.
scripts/verify-recipes.pynpm test- —
references/subprocess.md规则,包括故障模式、代码和平台注意事项。sp- - —
references/threads.md规则:GIL、free-threading、所有权、锁和进程池行为。thread- - —
references/processes.md规则:启动方式、pickle、IPC 和进程池生命周期。proc- - — 使用命令诊断卡死、死锁、孤儿进程和僵尸进程。
references/debugging-hangs.md
运行 ,在当前解释器上重新检查本 skill 的示例和版本声明(repo tooling,已接入 ;直接执行,不要将其读入上下文)。它会为每项检查输出一行并注明对应规则,在非 POSIX 系统上跳过 POSIX 专属检查,并断言当前 Python 版本所对应的文档 API 最低版本。
scripts/verify-recipes.pynpm test