【问题标题】:Does Python's main thread get garbage collected when it stops?Python的主线程停止时是否会被垃圾收集?
【发布时间】:2013-06-04 23:07:32
【问题描述】:

在一个多线程 Python 进程中,我有许多 非守护进程 线程,我的意思是即使在主线程退出/停止后仍保持主进程处于活动状态的线程。 p>

我的非守护线程将weak references 保存到主线程中的某些对象,但是当主线程结束时(控制从文件底部脱落)这些对象确实似乎是垃圾收集,我的弱引用终结器回调不会触发。

我期望主线程被垃圾收集是错误的吗?我本来期望线程本地将被释放(即垃圾收集)......

我错过了什么?


辅助材料

pprint.pprint( threading.enumerate() ) 的输出显示主线程已停止,而其他人仍在继续。

[<_MainThread(MainThread, stopped 139664516818688)>,
 <LDQServer(testLogIOWorkerThread, started 139664479889152)>,
 <_Timer(Thread-18, started 139663928870656)>,
 <LDQServer(debugLogIOWorkerThread, started 139664437925632)>,
 <_Timer(Thread-17, started 139664463103744)>,
 <_Timer(Thread-19, started 139663937263360)>,
 <LDQServer(testLogIOWorkerThread, started 139664471496448)>,
 <LDQServer(debugLogIOWorkerThread, started 139664446318336)>]

而且由于总是有人询问用例...

我的网络服务偶尔会错过其实时截止日期(在最坏的情况下会导致整个系统故障)。事实证明,这是因为只要文件系统发脾气,记录(重要)调试数据就会阻塞。因此,我正在尝试改造一些已建立的专用日志库,以将阻塞 I/O 推迟到工作线程。

遗憾的是,已建立的使用模式是记录重叠并行事务的短期日志通道和从未显式关闭的长期模块范围通道的混合。

所以我创建了一个装饰器,它将方法调用推迟到工作线程。工作线程是非守护进程,以确保所有(慢)阻塞 I/O 在解释器退出之前完成,并持有对客户端(方法调用入队)的弱引用。当客户端被垃圾回收时,弱引用的回调触发并且工作线程知道没有更多的工作将被排队,因此将在下次方便时退出。

除了一个重要的用例之外,这似乎在所有情况下都可以正常工作:当日志记录通道位于主线程中时。当主线程停止/退出时,日志通道最终确定,因此我的(非守护程序)工作线程继续保持整个进程处于活动状态。

【问题讨论】:

  • 有很多假设必须在没有具体代码示例的情况下回答这个问题...也就是说,您是否尝试过在主线程末尾显式关闭/删除日志记录通道,只是为了看看弱引用的回调是否被触发了?

标签: python python-2.6 python-multithreading


【解决方案1】:

在所有非守护线程上不调用join 就结束主线程是个坏主意,或者对如果不这样做会发生什么做任何假设。


如果您没有做任何非常不寻常的事情,CPython(至少是2.0-3.3)将通过在所有非守护线程上自动调用join 作为_MainThread._exitfunc 对来为您服务。这实际上并没有记录在案,因此您不应该依赖它,但这就是发生在您身上的事情。

您的主线程实际上根本没有退出;它在其_MainThread._exitfunc 内部阻塞,试图join 一些任意的非守护线程。在调用 atexit 处理程序之前,它的对象不会被最终确定,直到它完成加入所有非守护线程之后才会发生。


同时,如果您避免这种情况(例如,通过直接使用thread/_thread,或者通过将主线程与其对象分离或将其强制为普通的Thread 实例),会发生什么?它没有定义。 threading 模块根本没有引用它,但是在 CPython 2.0-3.3 中,并且可能在任何其他合理的实现中,它由 thread/_thread module 来决定。而且,正如文档所说:

当主线程退出时,系统定义其他线程是否存活。在使用本机线程实现的 SGI IRIX 上,它们仍然存在。在大多数其他系统上,它们会在不执行 try ... finally 子句或执行对象析构函数的情况下被杀死。

因此,如果您设法避免 join 处理所有非守护线程,则您必须编写既可以处理像守护线程一样硬杀死它们,又可以让它们继续运行直到退出的代码。

如果它们确实继续运行,至少在 POSIX 系统上的 CPython 2.7 和 3.3 中,主线程的操作系统级线程句柄以及表示它的各种更高级别的 Python 对象可能仍会保留,而不会被清理由 GC。


最重要的是,即使所有内容都已发布,您也不能依赖 GC 删除任何内容。如果您的代码依赖于确定性 GC,在许多情况下您可以在 CPython 中摆脱它(尽管您的代码随后会在 PyPy、Jython、IronPython 等中中断),但在退出时不是其中之一。 CPython 可以并且将会在退出时泄漏对象并让操作系统对其进行排序。 (这就是为什么你永远不会关闭的可写文件可能会丢失最后几次写入的原因——__del__ 方法永远不会被调用,因此没有人告诉他们flush,至少在 POSIX 上,底层的FILE* 没有' t 自动冲洗。)

如果你想在主线程结束时清理一些东西,你必须使用某种close函数而不是依赖__del__,并且你必须确保它通过with触发围绕主要代码块、atexit 函数或其他一些机制进行阻塞。


最后一件事:

我原以为线程局部变量会被释放(即垃圾收集)......

你真的在某个地方有线程本地人吗?还是您只是指仅在一个线程中访问的局部变量和/或全局变量?

【讨论】:

  • 碰巧,我指的是只能在一个线程中访问的局部变量和/或全局变量。我将尝试使用线程的本地属性来看看它的作用。
  • 我从来没有听说过主线程在没有调用 join() 的情况下结束的非法性 - 你可以向我推荐一些关于这方面的文档吗?我发现自己想知道如果您必须以任何方式加入非守护线程有什么意义(肯定这样的用法会使线程与守护线程无法区分?)
  • @RobM:我认为你可能把事情搞反了。守护线程的重点是您加入它们;你只是让它们运行,当所有非守护线程退出时,它们会被突然杀死。非守护线程的重点是您可以并且确实加入它们。
  • @RobM:至于调用join的要求……这并不像我在初始版本中所说的那么清楚。我已经更新了答案以提供所有详细信息。但简短的版本是,很难编写正确的代码 没有 join 所有非守护线程,而您认为自己侥幸逃脱的唯一原因是threading 正在背后为你做joins。
猜你喜欢
  • 1970-01-01
  • 2011-01-26
  • 2011-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-15
  • 2018-01-06
  • 2023-03-21
相关资源
最近更新 更多