3

在多线程 Python 进程中,我有许多非守护线程,我的意思是即使在主线程退出/停止后也能保持主进程活动的线程。

我的非守护线程在主线程中持有对某些对象的弱引用,但是当主线程结束时(控制从文件底部脱落)这些对象似乎没有被垃圾收集,我的弱引用终结器回调没有不开火。

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

我错过了什么?


配套材料

显示主线程的输出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 在解释器退出之前完成,并持有对客户端的弱引用(方法调用入队)。当客户端被垃圾回收时,弱引用的回调触发并且工作线程知道没有更多的工作将被排队,因此将在下次方便时退出。

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

4

1 回答 1

3

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


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

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


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

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

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

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


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

如果你想在主线程完成时清理一些东西,你必须使用某种close函数而不是依赖__del__,并且你必须确保它通过with主代码块周围的块、atexit函数或其他一些机制。


最后一件事:

我本来期望线程本地将被释放(即垃圾收集)......

您实际上在某处有线程本地人吗?还是您只是指仅在一个线程中访问的本地和/或全局?

于 2013-06-04T23:45:27.560 回答