每当我在 python 3 中从子线程中断/终止睡眠父线程时,我都会得到这种奇怪的行为(抛出随机 TypeError 异常),但只有当 python 脚本是由带有
&
的 shell 脚本启动时才会出现这种奇怪的行为使其在后台运行的参数。
所以这是我能想到的可以触发这个问题的最小可重现的Python代码。
User@MSI: ~/test $ cat m.py
import threading
import _thread
import time
def child_thread():
time.sleep(1)
print('child interrupting parent')
_thread.interrupt_main()
if __name__ == '__main__':
t = threading.Thread(target=child_thread, args=())
t.start()
print('parent looping')
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print('caught interruption raised from user or child thread :)')
except TypeError as e:
print('why would I ever catch a TypeError?')
raise
except Exception as e:
print('Strange Exception Received ')
raise
这是一个非常简单的脚本,其中父线程启动子线程,然后永远循环,子线程(1 秒后)中断主线程,应该向主线程引发键盘中断。
现在,如果我使用最后带有背景标志
&
的 bash 脚本调用我的脚本,
(下面的输出没有任何意义)
User@MSI: ~/test $ cat ./m.sh
#!/bin/bash
python3 m.py &
User@MSI: ~/test $ ./m.sh
parent looping
child interrupting parent
why would I ever catch a TypeError?
Traceback (most recent call last):
File "m.py", line 17, in <module>
time.sleep(1)
TypeError: 'int' object is not callable
下面我显示了我的脚本的输出,当我运行它时它表现完全正常
三个人的行为都符合预期...... (以下输出符合预期)
User@MSI: ~/test $ python3 m.py
parent looping
child interrupting parent
caught interruption raised from user or child thread :)
使用 & 在后台运行时的结果相同 (以下输出符合预期)
User@MSI: ~/test $ python3 m.py &
[1] 5884
parent looping
child interrupting parent
caught interruption raised from user or child thread :)
[1]+ Done python3 m.py
甚至从一个名为 m.sh 的一行脚本执行 m.py (以下输出符合预期)
User@MSI: ~/test $ cat m.sh
#!/bin/bash
python3 m.py
User@MSI: ~/test $ ./m.sh
parent looping
child interrupting parent
caught interruption raised from user or child thread :)
我完全傻眼了,不知道 time.sleep 和 TypeErrors 与我如何调用我的脚本,特别是从 shell 脚本和后台调用它有什么关系。这是我遇到的最奇怪的错误之一。
如果重要的话,我正在跑步
Python 3.6.12
和 Debian 9.12
我希望有人能解决这个问题。
编辑:以下是有缺陷版本的字节码比较(从 shell 脚本使用 & 运行)输出 这是好的版本(从终端运行)输出
为了方便比较,这里是字节码的
diff
。唯一的区别是子线程在内存中的位置。
这是 CPython 中的一个错误,issue23395。
它已在 Python 3.8 中由 PR #7778 修复。 变更日志在 3.8.0 beta 1 的最后一个“库”项目中提到了它,其中写道:
如果 Python 忽略或不处理
_thread.interrupt_main()
信号,现在可以避免设置 Python 错误状态。SIGINT
该修复程序也已在 PR #13541 中反向移植到 Python 3.7,因此您应该会发现它可以在 Python 3.7.3 中重现,然后在 Python 3.7.4 中修复。
Thomas Kluyver 解释为什么它是
TypeError
:
此操作失败,并显示错误消息“TypeError: 'int' object is not callable”,并且回溯与错误原因完全断开连接,可能是因为它不是来自通常的 Python 堆栈。
问题似乎是,interrupt_main 设置了(在 C 代码中)Handlers[SIGINT].tripped,这仅当处理程序是 Python 函数时才会发生。当 PyErr_CheckSignals() 运行时,它尝试将 Handlers[SIGINT].func 作为 Python 函数调用,但它是一个 Python 整数,从而导致错误。
你是对的,
TypeError
回溯“没有意义”。
仅当 Python 脚本
m.py
从 bash 脚本 m.sh
中的后台任务运行时才会发生这种情况,因为这种方式的作业控制(挂起/恢复任务的能力)被禁用。当作业控制被禁用时,Python会将signum 2处理程序设置为signal.SIG_IGN,从而忽略中断。否则,signum 2 处理程序将为 signal.default_int_handler
,这会引发 KeyboardInterrupt
异常。这不是 CPython 特有的东西,而是 POSIX 的东西(ref)。
您可以通过在 bash 脚本中添加
set -m
(即打开“监视”shell 选项)来确认这一点:
#!/bin/bash
set -m
python3 m.py &
这也应该避免触发错误。