在做 Python 循环引用垃圾回收实验中的一个小问题, Python3 的 print 是线程安全的吗?

2020-03-13 17:32:41 +08:00
 lithbitren
以前都听说 Python 循环引用会导致引用数无法清零,所以不能垃圾回收,会内存泄漏,需要删除引用关系或者用 gc.collect()才能进行正常垃圾回收。

但做了下实验,好像还是会自动回收循环引用的变量,在约 44 对循环引用变量时会首次清理,之后大约每产生 200 对的垃圾就会清一次,但也没清干净,总是会有残留上几个,而且听说了改__del__会影响垃圾回收,实质上也不会影响。

进程内存监控,可以看出内存大小基本没变,还是符合认知的。

====运行环境====

win10 1903
Python 3.7.5 64bit

=====代码=====

import psutil

class A:
ㅤt = 0
ㅤdef __del__(self):
ㅤㅤA.t += 1

class B:
ㅤt = 0
ㅤdef __del__(self):
ㅤㅤB.t += 1

if __name__ == '__main__':
ㅤpid = os.getpid()
ㅤat, bt = 0, 0
ㅤfor i in range(10000):
ㅤㅤa, b = A(), B()
ㅤㅤa.b, b.a = b, a
ㅤㅤ#del a, b
ㅤㅤif A.t != at or B.t != bt:
ㅤㅤㅤprint(f'No_{i}: (d_i: {i - at}, d_a: {A.t - at}, d_b: {B.t - bt})')
ㅤㅤㅤprint('Used Memory:', psutil.Process(pid).memory_info().rss / 1024 / 1024, 'MB')
ㅤㅤㅤat, bt = A.t, B.t

====打印结果====

No_44: (d_i: 44, d_a: 43, d_b: 43)
Used Memory: 13.69921875 MB
No_236: (d_i: 193, d_a: 190, d_b: 190)
Used Memory: 13.8046875 MB
No_431: (d_i: 198, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_626: (d_i: 200, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_822: (d_i: 202, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_1017: (d_i: 203, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_1212: (d_i: 204, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_1407: (d_i: 206, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_1603: (d_i: 208, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_1798: (d_i: 209, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_1993: (d_i: 210, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_2188: (d_i: 212, d_a: 212, d_b: 212)
Used Memory: 13.8046875 MB
No_2384: (d_i: 196, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_2579: (d_i: 197, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_2774: (d_i: 198, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_2969: (d_i: 200, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_3165: (d_i: 202, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_3360: (d_i: 203, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_3555: (d_i: 204, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_3750: (d_i: 206, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_3946: (d_i: 208, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_4141: (d_i: 209, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_4336: (d_i: 210, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_4531: (d_i: 212, d_a: 211, d_b: 211)
Used Memory: 13.8046875 MB
No_4727: (d_i: 197, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_4922: (d_i: 198, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_5117: (d_i: 199, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_5312: (d_i: 201, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_5508: (d_i: 203, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_5703: (d_i: 204, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_5898: (d_i: 205, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_6093: (d_i: 207, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_6289: (d_i: 209, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_6484: (d_i: 210, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_6679: (d_i: 211, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_6874: (d_i: 213, d_a: 211, d_b: 211)
Used Memory: 13.8046875 MB
No_7070: (d_i: 198, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_7265: (d_i: 199, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_7460: (d_i: 200, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_7655: (d_i: 202, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_7851: (d_i: 204, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_8046: (d_i: 205, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_8241: (d_i: 206, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_8436: (d_i: 208, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_8632: (d_i: 210, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_8827: (d_i: 211, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_9022: (d_i: 212, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_9217: (d_i: 214, d_a: 211, d_b: 211)
Used Memory: 13.8046875 MB
No_9413: (d_i: 199, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_9608: (d_i: 200, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB
No_9803: (d_i: 201, d_a: 193, d_b: 193)
Used Memory: 13.8046875 MB
No_9998: (d_i: 203, d_a: 194, d_b: 194)
Used Memory: 13.8046875 MB

====实验结束====

之前了解 Python3 的 print 是线程安全的,但在这个垃圾回收的实验里,如果在__del__函数里添加 print,在自动回收垃圾时有一定概率(概率小于 20%)报出非阻塞的错误。
不过似乎只在 Power shell 会报错,在 cmd 里似乎不会报错,但考虑到 cmd 的输出远快于 Power shell,可能是达不到并发标准所以在 cmd 才不报错。
而且是在此期间所有同时被回收的对象都会同时报错,还不太了解什么原因,不知道是不是并发回收对象的问题。

报错信息如下,一般一出就是 4 百条同时来:

RuntimeError: reentrant call inside <_io.BufferedWriter name='<stdout>'>
3125 次点击
所在节点    Python
11 条回复
limyel
2020-03-13 17:43:59 +08:00
我记得循环引用不是用标记清除来解决吗
lithbitren
2020-03-13 17:45:59 +08:00
@limyel 是的,一直都是这么听说的,如果循环引用数量少的话(少于 44 对时),在__del__里发打印,会发现程序退出的时候才会打印。
lithbitren
2020-03-13 17:49:49 +08:00
顺便问问怎么在输入框里正常贴代码,直接贴好像缩进会被铲掉。
ipwx
2020-03-13 17:51:02 +08:00
Python 因为有 ref-counter,所以标记清扫很懒惰的吧。也是因为更推荐能用 weakref 就用 weakref
ppyybb
2020-03-13 19:39:53 +08:00
错误挺明显了,print 是个不可重入的函数,你在 del 里面写,会导致 print 可能被调用了一半后中断切到另外一个 del 里面又执行 print,导致报错。详情可以去知乎搜灵剑老大关于 gc 导致的不安全问题的文章。这个和线程安全不一样,是在一个线程里面切换的。如果 print 线程不安全,那么平常你 call print 也没有加锁啊……
lithbitren
2020-03-13 22:55:09 +08:00
@ppyybb 谢谢大佬提示,学 c 的时候学过不过那时还不了解并行,把函数的重入性和线程安全搞混了,后来玩 python 几乎不会出线程相关错误,所以是第一次见这个错误,错误信息直接丢网上大多都在说线程安全问题,所以以为就是线程安全问题,现在搜了搜资料大概是了解。

灵剑大大的那篇文章也没具体说什么情况会报错,感觉这应该算是一个例子吧。

不过灵剑大大在评论区里提到了 pypy,pypy 很多扩展和函数都不能直接用,不过测了下好像也是会自动 GC 的,不过启动阈值很大, 而且并不是成对清理的,开 n = 1 000 000 才发现__del__函数的调用。psutil 不能用,任务管理器里进程内存的变化肉眼不可,感觉还是挺黑箱的。

pypy7.3

No_188294: (d_i: 188294, d_a: 188293, d_b: 40108)
No_327273: (d_i: 138979, d_a: 138979, d_b: 148185)
No_482999: (d_i: 155726, d_a: 155726, d_b: 138979)
No_623060: (d_i: 140061, d_a: 140061, d_b: 155726)
No_758523: (d_i: 135463, d_a: 135463, d_b: 140061)
No_914568: (d_i: 156045, d_a: 156045, d_b: 135463)
lxy42
2020-03-13 23:14:12 +08:00
可能是 print 执行过程中触发了 GC, 然后 GC 回收对象时又执行了__del__中的 print, 因为 print 无法重入, 导致报错.
ppyybb
2020-03-13 23:23:45 +08:00
@lithbitren 是比较黑箱,不知道这个知识点的就会被坑,所以最好不要在 del 里面做这些操作,很难控制
monsterxx03
2020-03-13 23:38:16 +08:00
在 3.4 之前,之前如果一个 class 内部有循环引用,并重载了 del,的确会内存泄漏,celery4.1 就有这个 bug https://github.com/celery/celery/pull/4839
felix021
2020-03-14 00:16:29 +08:00
CPython 1.x 仅仅使用引用计数来实现垃圾回收。虽然引用计数易于实现和理解,但是它不足以解决循环引用的问题。于是乎在 1999 年实现了一个循环垃圾回收器,并且附加在了 Python 2 以后的版本上。
lithbitren
2020-03-14 00:25:01 +08:00
垃圾回收好像看到说用通过链表来解决不可到达区,但为什么会有残留呢?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/652561

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX