【问题标题】:How can I tell where my python script is hanging?如何判断我的 python 脚本挂在哪里?
【发布时间】:2011-03-27 11:53:24
【问题描述】:

所以我正在调试我的 python 程序并且遇到了一个导致程序挂起的错误,就像在一个无限循环中一样。现在,我之前遇到了无限循环的问题,但是当它挂断时,我可以终止程序,python 吐出一个有用的异常,告诉我当我向它发送 kill 命令时程序在哪里终止。但是,现在,当程序挂起并且我 ctrl-c 时,它并没有中止而是继续运行。有什么工具可以用来定位挂断吗?我是分析新手,但据我所知,分析器只能为您提供有关已成功完成的程序的信息。或者您可以使用分析器来调试此类挂断吗?

【问题讨论】:

  • 你怎么知道它在循环中?一个 CPU 是否固定为 100%?如果不是,它可能处于套接字等待状态(假设它正在执行网络 I/O)。
  • 如果它没有响应 Ctrl+C,那可能是因为 KeyboardInterrupt 异常在某个时候被捕获。如果您有一个未命名任何特定异常类的 try: ... except: 子句,则可能是原因。
  • 我遇到了完全相同的问题,并且 Ctrl+C 不起作用,因为它挂在某处的 C 调用中。我的 CPU 正在 100% 运行。

标签: python debugging


【解决方案1】:

假设您正在运行您的程序:

python YOURSCRIPT.py

尝试运行您的程序:

python -m trace --trace YOURSCRIPT.py

在屏幕上打印大量内容时请耐心等待。如果你有一个无限循环,它将永远持续下去(停止问题)。如果它卡在某个地方,那么主要是您卡在了 I/O 上,或者这是一个死锁。

【讨论】:

  • +1。您可以使用 --ignore-dir 或 --ignore-module 选项来减少输出量,例如阻止它跟踪所有标准模块。您还可以将输出重定向到文件以供以后检查。
【解决方案2】:

哇!已经有 5 个答案,但没有人提出最明显和最简单的答案:

  1. 尝试找到导致挂起行为的可重现测试用例。
  2. 将日志记录添加到您的代码中。这可以像print "**010"print "**020" 等一样基本,贯穿主要领域。
  3. 运行代码。看看它挂在哪里。不明白为什么?添加更多日志记录。 (即如果在 **020 和 **030 之间,请添加 **023、**025、**027 等)
  4. 转到 3。

【讨论】:

  • +1,这是我经常做的。当然,调试器和 IDE 也很有用,但我发现这是确定 bug 位置的最快/最简单的方法,因为我已经大致了解要在源代码的哪个部分查找它。(只是我的意见,当然)
  • 对于简单的脚本,没有复杂性,这是可行的。但是,对于长时间运行的复杂程序,这是没有用的。
  • @xlash 对于复杂的程序,您将“**010”更改为“启动冲击波模块”并将“**020”更改为“连接到融合服务器”等。更改“打印”以使用“记录”模块。如果你不这样做,那你就错了。
  • 不,代码太大了。不能到处添加打印语句。必须有更好的答案
  • @Mayou36 我经常使用多种语言编写超过 100KLOC 的代码库,并且已经编程了 30 多年。我怀疑这里有某种邓宁-克鲁格效应在起作用。 'print "**010"' 的概念示例是为了简单起见。日志记录是必不可少的。去做吧!
【解决方案3】:

我编写了一个模块,可以打印出在一个地方挂起超过 10 秒的线程。 hanging_threads.py

运行:

python -m pip install hanging_threads

将此添加到您的代码中:

from hanging_threads import start_monitoring
start_monitoring(seconds_frozen=10, test_interval=100)

这是一个示例输出:

--------------------    Thread 5588     --------------------
  File "C:\python33\lib\threading.py", line 844, in _exitfunc
        t.join()
  File "C:\python33\lib\threading.py", line 743, in join
        self._block.wait()
  File "C:\python33\lib\threading.py", line 184, in wait
        waiter.acquire()

当您忘记将另一个线程设置为守护进程时,这会在主线程退出时发生。

【讨论】:

  • 这个工具正是我在生产中的 uWSGI 下在我的 Django 应用程序中寻找瓶颈的工具。我认为做一个python包是值得的。如果将来发生这种情况,请告诉我们。
  • @raacer,它是一个包,现在:pypi.python.org/pypi/hanging_threads
  • 这对我来说非常有效。我建议您在答案中附加 pip install 命令和在代码顶部添加的 sn-p 以启用模块。
  • 像魅力一样工作!谢谢用户。我已将 pip 命令和用法 sn-p 添加到您的答案中。
  • 从 Python 3.3 开始有faulthandler 来解决这个问题。
【解决方案4】:

如果您的程序太大太复杂而无法使用 pdb 单步执行或使用跟踪模块打印每一行,那么您可以尝试我在 8 位游戏编程时代的技巧。从 Python 2.5 开始,pdb 可以使用 commands 命令将代码与断点关联起来。您可以使用它来打印消息并继续运行:

(Pdb) commands 1
(com) print "*** Breakpoint 1 ***"
(com) continue
(com) end
(Pdb)

当断点 1 被命中时,这将打印一条消息并继续运行。为其他几个断点定义类似的命令。

您可以使用它对您的代码进行某种二进制搜索。在代码的关键位置附加断点并运行它直到它挂起。您可以从最后一条消息中看出它命中的最后一个断点。然后,您可以移动其他断点并重新运行以缩小代码中挂起的位置。冲洗并重复。

顺便说一句,在 8 位微控制器(Commodore 64、Spectrum 等)上,您可以将一个值插入注册表位置以更改屏幕边框的颜色。我曾经设置几个断点来用不同的颜色来做这件事,所以当程序运行时,它会给出一个迷幻的彩虹显示,直到它挂起,然后边框会变成一个单一的颜色,告诉你最后一个断点是什么。您还可以通过彩虹中每种颜色的数量来很好地了解不同代码部分的相对性能。有时我会怀念这些新奇的“Windows”机器的简单性。

【讨论】:

    【解决方案5】:

    从 Python 3.3 开始,有一个内置的 faulthandler 模块。在发生通常致命的信号时打印所有线程的堆栈跟踪:

    import faulthandler
    faulthandler.enable()
    

    对于挂起的进程,设置故障处理程序以按需打印堆栈跟踪更为有用。这可以通过以下方式完成:

    import faulthandler
    import signal
    faulthandler.register(signal.SIGUSR1.value)
    

    然后,一旦进程挂起,您可以发送一个信号来触发堆栈跟踪的打印:

    $ python myscript.py &
    [1] <pid> 
    $ kill -s SIGUSR1 <pid>
    

    此信号不会终止进程,您可以多次发送以查看执行过程中不同点的堆栈跟踪。

    请注意,signal.SIGUSR1 需要 Python 3.5 或更高版本。对于旧版本,您只需硬编码信号编号(大多数常见的 linux 架构为 10)。

    faulthandler.dump_traceback 可以与threading.enumerate 一起使用,以识别具有daemon=False 的线程,从而通过hex(t.ident) 的十六进制ID 缩小到挂起线程。

    【讨论】:

      【解决方案6】:

      你也可以试试http://code.activestate.com/recipes/576515-debugging-a-running-python-process-by-interrupting/。只要 Python 进程没有屏蔽信号,它就应该可以工作,即使 Ctrl-C 不起作用也是如此。

      【讨论】:

        【解决方案7】:

        如果您的程序有点过于复杂而无法简单地跟踪所有功能,您可以尝试运行它并手动附加一个跟踪程序,例如lptrace。它的工作方式有点像strace——它打印出你的程序调用的每个函数。下面是如何调用它:

        python lptrace -p $STUCK_PROGRAM_PID
        

        请注意,lptrace 需要 gdb 才能运行。

        【讨论】:

        • 我试过这个 lptrace。现在我有两个 python 进程挂起。可爱。
        【解决方案8】:

        没有什么比以前的好pdb

        import pdb
        pdb.run('my_method()',globals(),locals())
        

        然后点击 (n) 进入下一个命令,(s) 进入。请参阅文档以获取完整参考。 按照你的程序一步一步来,你可能会很快搞清楚。

        【讨论】:

        • "你可能会很快搞清楚"....除非你的程序不小。
        【解决方案9】:

        防止这些挂起比调试它们更容易。

        首先:for 循环非常非常难以陷入循环不会终止的情况。很难。

        第二:while 循环比较容易卡在循环中。

        第一遍是检查每个while 循环,看看它是否必须while 循环。通常你可以用for 替换while 结构,然后你会通过重新思考你的循环来纠正你的问题。

        如果您不能用for 替换while 循环,那么您只需证明while 语句中的表达式必须在每次循环中都发生变化。这并不难证明。

        1. 查看循环中的所有条件。称之为T

        2. 查看循环体中的所有逻辑分支。有没有办法在不改变条件的情况下通过循环,T

          • 是吗?那是你的错误。那个逻辑路径是错误的。

          • 不是吗?太好了,该循环必须终止。

        【讨论】:

        • 这仅对单线程应用程序有效。在开发使用多线程的 Qt 应用程序时,我遇到过死锁、竞争和其他问题,例如信号和插槽。不,GIL 不会阻止所有这些。
        • 我完全不同意。防止这样的挂断基本上是不可能的。这些挂断将不可避免地发生。另一方面,调试几乎总是可能的,并且是软件开发的根本重要部分。
        【解决方案10】:

        多线程守护进程;使用pyrasite检查正在运行的程序

        我有一个多线程守护程序,有时会在数小时后卡住,有时在数周后卡住。通过调试器运行它是不可行的,甚至可能没有帮助,因为调试多线程或多进程程序可能会很痛苦。通过跟踪运行它可能会在卡住之前填满千兆字节(如果不是 TB 的话)。守护程序第二次出现挂起时,我想立即知道它在哪里,无需重新启动它,添加检查代码,通过调试器运行它,等待数小时、数天或数周它会再次挂起,以待调查。

        pyrasite 救了我,它允许用户连接到正在运行的 Python 进程并以交互方式检查帧(受this gist 启发的示例):

        $ pyrasite-shell 1071  # 1071 is the Process ID (PID)
        Pyrasite Shell 2.0
        Connected to '/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/bin/python3.8 /opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/bin/satpy_launcher.py -n localhost /opt/pytroll/pytroll_inst/config/trollflow2.yaml'                                                                                               
        Python 3.8.6 | packaged by conda-forge | (default, Dec 26 2020, 05:05:16)
        [GCC 9.3.0] on linux
        Type "help", "copyright", "credits" or "license" for more information.
        (DistantInteractiveConsole)
        
        >>> import sys
        >>> sys._current_frames()
        {139652793759488: <frame at 0x7f034b2c9040, file '<console>', line 1, code <module>>, 139653520578368: <frame at 0x7f034b232ac0, file '/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/pyresample/spherical.py', line 112, code __init__>}
        

        第一帧没有信息;那是我们自己的硫铁矿壳。然而,第二帧显示当前我们的脚本卡在第 112 行的模块 pyresample.spherical 中。我们可以使用 traceback 模块获取完整的回溯:

        >>> import traceback
        >>> traceback.print_stack(list(sys._current_frames().values())[1])
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/bin/satpy_launcher.py", line 80, in <module>
            main()
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/bin/satpy_launcher.py", line 75, in main
            run(prod_list, topics=topics, test_message=test_message,
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/trollflow2/launcher.py", line 152, in run
            proc.start()
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/multiprocessing/process.py", line 121, in start
            self._popen = self._Popen(self)
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/multiprocessing/context.py", line 224, in _Popen
            return _default_context.get_context().Process._Popen(process_obj)
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/multiprocessing/context.py", line 277, in _Popen
            return Popen(process_obj)
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/multiprocessing/popen_fork.py", line 19, in __init__
            self._launch(process_obj)
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/multiprocessing/popen_fork.py", line 75, in _launch
            code = process_obj._bootstrap(parent_sentinel=child_r)
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
            self.run()
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/multiprocessing/process.py", line 108, in run
            self._target(*self._args, **self._kwargs)
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/trollflow2/launcher.py", line 268, in process
            cwrk.pop('fun')(job, **cwrk)
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/trollflow2/plugins/__init__.py", line 403, in covers
            cov = get_scene_coverage(platform_name, start_time, end_time,
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/trollflow2/plugins/__init__.py", line 425, in get_scene_coverage
            return 100 * overpass.area_coverage(area_def)
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/trollsched/satpass.py", line 242, in area_coverage
            inter = self.boundary.contour_poly.intersection(area_boundary)
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/pyresample/spherical.py", line 494, in intersection
            return self._bool_oper(other, -1)
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/pyresample/spherical.py", line 475, in _bool_oper
            inter, edge2 = edge1.get_next_intersection(narcs2, inter)
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/pyresample/spherical.py", line 326, in get_next_intersection
            return None, None
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/pyresample/spherical.py", line 298, in intersection
            return None
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/pyresample/spherical.py", line 264, in intersections
            return (SCoordinate(lon, lat),
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/pyresample/spherical.py", line 62, in cross2cart
            return res
          File "/opt/pytroll/pytroll_inst/miniconda3/envs/pytroll-py38/lib/python3.8/site-packages/pyresample/spherical.py", line 112, in __init__
            self.cart = np.array(cart)
        

        我们可以使用 Python 内省的所有功能来检查堆栈,以帮助我们重建卡住的情况。

        【讨论】:

          【解决方案11】:

          我自己没有使用过,但我听说Eric IDE 很好,并且有一个很好的调试器。这也是我所知道的唯一具有 Python 调试器的 IDE

          【讨论】:

          • Wing IDE 也有一个很好的调试器——强烈推荐。问候。
          【解决方案12】:

          如果您的程序有多个线程,它可能会忽略 ctrl-c,因为一个线程连接到 ctrl-c 处理程序,但活动(失控?)线程对其充耳不闻。 CPython 中的 GIL(全局解释器锁)意味着通常任何时候实际上只能运行一个线程。 我想我用this解决了我的(也许)类似的问题

          【讨论】:

            【解决方案13】:
            i = 0
            for t in threading.enumerate():
                if i != 0:# and t.getName() != 'Thread-1':
                    print t.getName()
                    t._Thread__stop()
                i += 1
            

            一旦您知道线程的名称;开始重新执行您的脚本并将它们过滤掉,而不是阻止它们被中止。 i=0 条件防止主线程被中止。

            我建议检查并命名所有线程;如: 线程(target=self.log_sequence_status, name='日志状态')

            这段代码应该放在启动失控进程的主程序的末尾

            【讨论】:

              【解决方案14】:

              哇!似乎您在没有测试的情况下一次性添加了这么多代码,以至于您无法说出在程序开始挂起之前添加了什么代码......(最可能的问题原因)。

              说真的,您应该按小步骤编写代码并单独测试每个步骤(最好是进行 TDD)。

              对于您发现什么 python 代码正在运行并且 ctrl-c 不起作用的确切问题,我将尝试一个原始的猜测:您是否使用了一些 except: 模糊地捕获所有异常。如果您在循环中执行此操作(并在管理异常后继续循环),则很可能是 ctrl-c 不起作用的原因:它被此异常捕获。更改为except Exception:,它不应再被捕获(ctrl+c 可能无法像另一张海报所建议的那样像线程管理那样工作,但我相信上述原因更有可能)。

              键盘中断异常

              Raised when the user hits the interrupt key (normally Control-C or Delete). 
              

              在执行期间,会定期检查中断。 内置函数 input() 或 raw_input() 时输入的中断 等待输入也会引发此异常。异常继承 来自 BaseException 以免被以下代码意外捕获 捕获异常,从而阻止解释器退出。

              Changed in version 2.5: Changed to inherit from BaseException.
              

              【讨论】:

              • 我只是想知道这个答案的哪一部分在写完两年后得到了这个-1。最后添加的代码是可能导致新的不良行为的代码,您应该使用 TDD,或者如果 Ctrl+C 没有回答问题可能是由于捕获键盘异常(并且可能在循环中)引起的。看起来周围有一些质量很差的评论者......
              • 保持不投票的人静音。我猜是 TDD 鄙视者或相信大型程序应该编写一次并且只在完成后运行(和调试)。
              • 不是一个反对者,但我明白为什么:如果你在谷歌上寻求帮助并找到这个页面......对于第一部分来说为时已晚并且没有帮助,但第二部分几乎可以肯定正确(并且值得一票) - 在您的例外情况中具体说明。甚至可能使用 TDD,您会遇到代码挂起的情况,确定这是自上次提交以来发生的事情……但是什么。 (FWIW 我遇到了这种情况,将代码库移植到 python 3,它已经过测试 - 现在大多数测试都通过了 - 但是一些奇怪的行为导致测试“挂起”。)
              • 有时问题发生在一些用户操作之后,而不是在提交之后。您的建议很好,但并非适用于所有情况。
              • @kriss 我是一名软件开发人员,实际上是因为现实世界代码库中的现实世界问题而来到这里的。在现实世界中,外部环境一直在自行变化。例如,在这种情况下,问题是数据库死锁,导致 Django 在启动时挂起。没有代码更改,这纯粹是数据库中的一个问题。仅仅因为你的代码不应该在你没有触及它时随机停止工作并不意味着它不会在实践中。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2021-03-27
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多