春江暮客

春江暮客的个人学习分享网站

Python 脚本卡住怎么办:用 faulthandler 查看线程堆栈

2026-10-12 技术
Python 脚本卡住怎么办:用 faulthandler 查看线程堆栈

批处理脚本不再输出日志,进程却还在运行。直接重启会丢掉现场:它是在等锁、等网络,还是主线程一直等工作线程结束?本文用 Python 标准库 faulthandler 记录线程当前执行位置,再按堆栈找等待点。

示例只依赖 Linux 和普通启用 GIL 的 CPython 3.10+,不需要 pip 包。代码已在 Linux、Python 3.11.4 上验证;人为制造的锁等待会在指定时间后解除,不会永久卡住。

1. 选择诊断方法

  • 方法一:定时快照。 提前在程序里配置,每隔几秒输出一次堆栈,适合无人值守任务。
  • 方法二:按需快照。 提前注册 SIGUSR1,遇到停滞时从另一终端触发,适合 Linux 现场排查。

Python 官方文档说明了定时输出和取消接口。本文的 exit=False 保留现场,快照本身不会结束任务。python3 -X faulthandler 主要启用故障信号处理;它不会自动设置定时输出或注册 SIGUSR1。

如果任务根本没有启动,先按cron 环境排查教程检查解释器和工作目录。

2. 准备并保存完整示例

  1. 新建一个练习目录,确认解释器:
mkdir python-hang-demo
cd python-hang-demo
python3 --version
  1. 下载 hang_demo.py 到该目录,或把下面代码保存为同名文件:
"""Inspect a bounded lock wait using CPython 3.10+ on Linux."""
import argparse
import faulthandler
import os
import signal
import threading
from pathlib import Path


def worker(lock):
    with lock:
        print("WORKER: acquired lock", flush=True)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--log", type=Path, default=Path("stacks.log"))
    parser.add_argument("--seconds", type=int, default=8)
    parser.add_argument("--interval", type=int, default=2)
    args = parser.parse_args()
    if not (1 <= args.seconds <= 3600 and 1 <= args.interval <= 3600):
        parser.error("seconds and interval must be between 1 and 3600")

    lock = threading.Lock()
    # Exclusive creation preserves stack logs from previous runs.
    with args.log.open("x", encoding="utf-8") as stack_log:
        faulthandler.register(signal.SIGUSR1, file=stack_log, all_threads=True)
        try:
            faulthandler.dump_traceback_later(
                args.interval, repeat=True, file=stack_log, exit=False
            )
            with lock:
                thread = threading.Thread(target=worker, args=(lock,))
                thread.start()
                # Registration is complete before these readiness messages.
                print(f"READY pid={os.getpid()}", flush=True)
                print(f"SNAPSHOT: kill -USR1 {os.getpid()}", flush=True)
                thread.join(timeout=args.seconds)
            # Releasing the lock lets the waiting worker finish.
            thread.join()
        finally:
            faulthandler.cancel_dump_traceback_later()
            faulthandler.unregister(signal.SIGUSR1)
    print(f"DONE: {args.log}", flush=True)


if __name__ == "__main__":
    main()
  1. 查看命令参数:
python3 hang_demo.py --help

--seconds 是演示中主线程持锁等待的时间;--interval 是快照间隔。两者单位均为秒,范围为 1–3600。代码只用于独立练习进程;接入已有服务前,先确认服务没有把 SIGUSR1 用于其他用途。

3. 方法一:定时记录等待点

python3 hang_demo.py --log timer.log --seconds 8 --interval 2
cat timer.log

终端会先显示 READY 和 SNAPSHOT,约八秒后显示 WORKER: acquired lock 与 DONE: timer.log。日志中会有 Timeout (0:00:02)! 和线程堆栈;临界时刻的快照次数可能略有差异。

观察两处位置:工作线程停在 worker 的 with lock:,主线程位于 thread.join(timeout=args.seconds),上面通常还有 threading.py 的等待帧。按官方 Thread.join 说明,超时只结束这次等待,不会终止线程;本例随后退出 with lock,释放锁,工作线程才继续运行。

这说明主线程一边持锁,一边等需要同一把锁的工作线程。真实代码应把 join() 移到持锁区外,而不是仅把等待时间调大。下面是可单独运行的修正示例:

import threading

lock = threading.Lock()

def worker():
    with lock:
        print("WORKER: acquired lock")

thread = threading.Thread(target=worker)
thread.start()
thread.join(timeout=2)
assert not thread.is_alive(), "worker did not finish"
print("OK: joined without holding the lock")

4. 方法二:用 SIGUSR1 立即输出堆栈

  1. 在终端 A 的练习目录运行:
python3 hang_demo.py --log signal.log --seconds 30 --interval 60
  1. 等到显示 READY,复制程序打印的整条 kill -USR1 命令,在终端 B 执行。然后在同一个练习目录读取:
cat signal.log

此处间隔设为 60 秒,演示会在约 30 秒后结束,因此文件里的快照来自手动信号。发信号后任务继续运行;终端 A 最后仍会显示 DONE: signal.log。

只向打印该命令、仍在运行的练习进程发送信号。SIGUSR1 是用户自定义信号,具体行为由接收进程决定。没有注册处理器时,不要把它当作通用“查看 Python 堆栈”命令;本文也不能给一个已经卡住且未接入诊断的进程临时注入处理器。

5. 接入自己的任务并验证

保留示例中打开文件、注册信号、设置定时器及 finally 清理的部分,把锁等待演示替换为实际工作函数。日志句柄必须在诊断期间保持打开;顺序是 取消定时器 → 注销信号 → 关闭文件。

每次运行选择新的堆栈文件。普通业务日志可以按日志轮转教程管理,但不要直接轮转这里的已注册句柄;文件描述符说明要求更换文件后重新配置诊断输出。

验证时同时检查:日志非空、有工作函数名、有主线程等待帧,且任务最终正常退出。若真正卡住的是另一个程序,应给那个进程接入诊断;Python 父进程的堆栈只会显示自己在等待,可结合子进程超时与日志教程管理外部命令。

6. 常见问题

现象 原因与处理
FileExistsError 已有同名日志。保留旧文件,改用 --log timer-2.log。
FileNotFoundError 日志父目录不存在。先执行 mkdir -p traces,再用 --log traces/run-1.log。
日志为空 程序可能在首次快照前结束。重跑示例时用新的日志名,设置 --seconds 8 --interval 2。
kill 报 No such process 演示已结束。重新启动,等待新 READY,使用新打印的命令。
多次快照停在同一行 检查那一行调用的锁、I/O 或等待对象;单凭位置相同还不能认定死锁。

本教程查看 Python 线程堆栈,不展开原生库内部调用,也不是性能分析器。每次运行只配置一套定时输出,避免不同模块互相覆盖;长时间重复快照会增加日志体积,应按运行时长选择间隔并管理历史文件。

7. 总结

给长任务提前配置定时快照或 SIGUSR1,停滞时就能留下等待位置。先用可结束的锁等待练习验证日志,再把诊断接入实际脚本。定位到阻塞调用后修正锁范围或设置合适的 I/O 超时,堆栈输出本身不会解除等待。

标签

1024 12306 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor automation bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena copy cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization debugging deployment desktop-app devtools disk-space disown docker dovecot download electron esm esm2 esm3 esmc esmfold2 faceswap fasta fastmcp faulthandler ffmpeg file-io flashppi flask folium frontend game generator git github-actions google google-research grep harness hls html http hugo indexnow javascript jev json just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pdb pep-723 phaser pillow pip postfix preprocessing print protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 signals sklearn solana somaticsignatures spl sqlite ssh standardize static-site subprocess sysadmin system-one tensorflow tkinter tron tronpy troubleshooting turtle typesafe-ai ubuntu usdt uv vhhbert vite webp wordcloud wordpress workflow yaml 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它