Python异常时的垃圾回收死锁

0 投票
1 回答
66 浏览
提问于 2025-04-14 18:10

我遇到了一个奇怪的情况,导致一个程序无法退出,这和Python处理异常的方式有关。在这个情况下,我有一个对象,它拥有一个线程,而这个线程只有在对象的__del__方法被调用时才会关闭。但是,如果程序因为与这个对象相关的异常而“退出”,那么这个异常会在它的堆栈跟踪中保留对这个对象的引用,这样就阻止了这个对象被删除。因为对象没有被删除,线程也就永远不会关闭,所以程序无法完全退出,最终就卡在那儿了。这里有一个小的示例:

import threading

class A:

  def __init__(self):
    self._event = threading.Event()
    self._thread = threading.Thread(target=self._event.wait)
    self._thread.start()

  def __del__(self):
    print('del')
    self._event.set()
    self._thread.join()

def main():
  a = A()
  # The stack frame created here holds a reference to `a`, which
  # can be verified by looking at `gc.get_referrers(a)` post-raise.
  raise RuntimeError()

main() # hangs indefinitely

一个解决方法是通过处理异常并抛出一个新的异常来打破这个引用链:

error = False
try:
  main()
except RuntimeError as e:
  error = True

if error:
  # At this point the exception should be unreachable; however in some
  # cases I've found it necessary to do a manual garbage collection.
  import gc; gc.collect()
  # Sadly this loses the stack trace, but that's what's necessary.
  raise RuntimeError()

有趣的是,即使没有任何异常,仅仅是在主模块中保留对a的引用,也会出现类似的问题:

A()  # This is fine, prints 'del'
a = A()  # hangs indefinitely

这到底是怎么回事?这是Python(3.10)的一个bug吗?有没有什么好的方法可以避免这些问题?我花了很长时间才弄明白发生了什么!

1 个回答

4

根据Python的数据模型

当解释器退出时,并不能保证仍然存在的对象会调用__del__()方法。

所以你不应该在__del__函数中结束线程。相反,建议在合适的时候明确设置一个标志来表示要结束,或者你可以使用上下文管理器:

import threading

class A:

  def __init__(self):
    self._event = threading.Event()
    
  def __enter__(self):
      self._thread = threading.Thread(target=self._event.wait)
      self._thread.start()
      
  def __exit__(self):
      self._event.set()
      self._thread.join()

def main():
  with A() as a:
    raise RuntimeError()

main()

撰写回答