bigywater.tech
Django · 109

Signal 的弱引用机制

connect 的 weak 参数默认是 True:信号只弱引用 receiver。常见的说法是「局部函数当 receiver 会被回收」。这篇按这个说法去试,结果和预期不一样,顺着查下去找到了原因。

局部函数没被回收
weak=True 下,gc.collect() 之后仍被调用(Django 6.1.1)
lru_cache(512)
签名缓存强引用了 connect 过的函数,挤满后才释放
weak=False
信号持有对象的强引用,对象不会被回收

局部函数会不会被回收

实测 · 本机执行输出
setup() 返回并 gc.collect() 之后 send:LOG = ['local called']

这和我一开始的预期相反,所以我没有直接下结论,而是接着查原因。

原因:签名缓存

实测 · 本机执行输出
缓存现状: CacheInfo(hits=0, misses=47, maxsize=512, currsize=47)
再 connect 600 个别的函数后: CacheInfo(hits=0, misses=647, maxsize=512, currsize=512)
send: []  LOG = []  receivers = 0

读 Django 源码:django/utils/inspect.py 里的 _get_func_parameters 被 functools.lru_cache(maxsize=512) 装饰,connect 检查 receiver 签名时会调用它,缓存把函数对象强引用住了。实测把缓存挤满(600 个别的函数)后,那个局部函数才被回收。也就是说 weak=True 的局部 receiver「什么时候消失」取决于缓存里还有多少别的函数——行为不确定,不要依赖它。这一点本文只在 Django 6.1.1 + Python 3.14 上验证,别的版本没有测。

lambda / partial / 模块级函数

实测 · 本机执行输出
被调用的: ['lambda', 'partial', 'module-level']

绑定方法

实测 · 本机执行输出
对象存在:n = 1  receivers = 1
对象 del 之后 send() 返回: []  receivers = 0

绑定方法用 WeakMethod:缓存里放的是底层函数,不是实例,所以实例本身可以被回收。

weak=False 的代价

实测 · 本机执行输出
weak=True: 删除对象引用后对象是否已被回收: True
weak=False: 删除对象引用后对象是否已被回收: False

弱引用的好处:receiver 是对象的方法时,对象不用再手动 disconnect。weak=False 让信号强引用,适合局部函数或 lambda(不然行为不确定),但持有的对象就不会释放。

分步演示

Signal 的弱引用
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。自建 Signal;Python 3.14 的 gc.collect();缓存的信息来自 django/utils/inspect.py(读源码)和 cache_info() 的实测
    • 没有验证:Django 其它版本 / Python 其它版本上同样的缓存行为、connect 之外的强引用来源、多线程下的回收时机
    京ICP备2026067502号-1京公网安备11010502063487号