Django 的信号机制
Django 将 signal 描述为“信号调度员”,主要以信号的形式,来触发多个应用程序。这篇文章将从源码分析的角度,讲解 Django 中 signal 的工作机制及使用方法。
Signal 类
signal 最常用的场景是通知,例如你的博客有了评论,系统会有一个通知的机制将评论推送给你。用 signal 实现的话,只需要在评论发布的时候触发信号通知,以此来代替将通知的逻辑放在评论发布之后,大大降低了程序耦合度,更利于系统后期的维护。
Django 中实现了一个 Signal 类,这个类用以实现“信号调度员”的功能,其工作机制如下图所示,主要分为两部分,一是每个需要被调度的 callback 函数注册到 signal 上,二是事件触发 sender
发送信号。
receiver
在 signal 中维护了一个列表 receiver
,里面记录的是连接到 signal 的回调函数及其 id
。其中每个 receiver
必须是回调函数,且接受关键词参数 **kwarg
, signal 的 connect
方法用来将回调函数连接到 signal 。
我们先来看看 connect
的源代码,如下。
class Signal:
...
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
from django.conf import settings
# If DEBUG is on, check that we got a good receiver
if settings.configured and settings.DEBUG:
assert callable(receiver), "Signal receivers must be callable."
# Check for **kwargs
if not func_accepts_kwargs(receiver):
raise ValueError("Signal receivers must accept keyword arguments (**kwargs).")
if dispatch_uid:
lookup_key = (dispatch_uid, _make_id(sender))
else:
lookup_key = (_make_id(receiver), _make_id(sender))
if weak:
ref = weakref.ref
receiver_object = receiver
# Check for bound methods
if hasattr(receiver, '__self__') and hasattr(receiver, '__func__'):
ref = weakref.WeakMethod
receiver_object = receiver.__self__
receiver = ref(receiver)
weakref.finalize(receiver_object, self._remove_receiver)
with self.lock:
self._clear_dead_receivers()
for r_key, _ in self.receivers:
if r_key == lookup_key:
break
else:
self.receivers.append((lookup_key, receiver))
self.sender_receivers_cache.clear()
代码十分清真,分为四部分:检查参数、获取 receiver
的 ID、 receiver
弱引用、加锁。这里我们主要看看后面两部分的内容。
receiver 弱引用
预备知识
弱引用:Python 中对垃圾回收的处理采用的是标记引用的方式(见文《Python 的垃圾回收机制》),而弱引用的作用在于避免循环引用导致内存泄漏。主要原理是在弱引用某对象时,不在引用标记中增加引用数,所以在该对象的强引用为 0 时,系统依然将其回收,此时弱引用失效。
method 和 function :Python 的函数与其他语言的一样,包含函数名和函数体,支持形参;与函数相比,方法多了一层类的关系,也就是说方法是定义在类里的函数。CPython 中对方法的定义是通过 PyMethod_New
函数,这个函数是通过 func
来一步步配置 method
的,看一段节选源代码:
PyObject *
PyMethod_New(PyObject *func, PyObject *self, PyObject *klass)
{
...
im->im_weakreflist = NULL;
Py_INCREF(func);
im->im_func = func;
Py_XINCREF(self);
im->im_self = self;
Py_XINCREF(klass);
im->im_class = klass;
_PyObject_GC_TRACK(im);
return (PyObject *)im;
}
method
中除了函数属性 im_func
以外,还有一个 im_self
属性表 self
,和 im_class
属性表 class
。
Bound Method 和 Unbound Method:方法又可以分为 bound
方法和 unbound
方法,区别在于 bound
方法多了一层实例绑定,也就是说, bound method
是通过实例调用方法,而 unbound method
是直接通过类调用方法。
signal 中的弱引用
熟悉 Python 垃圾回收的同学应该知道,当一个对象的引用计数为 0 时,Python 才对其进行垃圾回收。所以, signal 中所有对回调函数的引用默认均采用弱引用,以免造成内存泄漏。
首先, connect
的参数 weak
表示是否用弱引用,默认为 True
; receiver
可以是函数,也可以是方法,而 bound method
的引用是短暂的,与实例的生命周期一致,所以标准的弱引用不足以保持,需要采用 weakref.WeakMethod
来模拟 bound method
的弱引用;最后 weakref.finalize
方法返回一个可调用的终结器对象,当 receiver
被垃圾回收时调用,与普通弱引用不同的是,终结器在调用前将始终存活,被调用之后死亡,从而大大简化了生命周期管理。
加锁
锁的存在是为了实现线程安全,而线程安全是指在多个线程同时存在时,运行结果依然符合预期。显然,signal 中的 receiver
注册过程不是天生线程安全,signal 中实现线程安全的方法是加锁,来实现 connect
方法的原子操作。
锁在 signal 的 __init__
方法中定义的,采用的是标准库中的 Lock
:
self.lock = threading.Lock()
signal 用线程锁将清理 receiver
列表中的弱引用对象、 receiver
列表中增加元素、清理全局缓存字典这三个操作封装成了原子操作,如下:
with self.lock:
self._clear_dead_receivers()
for r_key, _ in self.receivers:
if r_key == lookup_key:
break
else:
self.receivers.append((lookup_key, receiver))
self.sender_receivers_cache.clear()
sender
准确的讲,signal 中的 sender
这是一个标识,用来记录是“谁”触发了 signal ,真正起作用的是 send
方法,这个方法就是在 event
中用来触发 signal 给所有 receiver
“发送消息”的。以下是 send
的源代码。
class Signal: ... def send(self, sender, **named):
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return []
return [
(receiver, receiver(signal=self, sender=sender, **named))
for receiver in self._live_receivers(sender)
]
不难看出,触发所有记录在案的回调函数,这个过程是同步的,所以 signal 不适合用来处理大批量任务,当然我们可以将其改写成异步任务。
signal 的使用方法
signal 的使用只需要配置两个地方,一个是回调函数的注册,一个是事件触发。
回调函数的注册有两种方式,一种是常规的 signal.connect()
;另外是 Django signal 提供了装饰器 receiver
,只需要传入是哪个 signal 即可完成装饰,也可以指定 sender
,如果不指定就接收所有的 sender
发送的信息。事件触发只需在可触发的地方调用 <your_signal>.send()
即可。下面给出一个 demo。
from django.dispatch import Signal, receiver
signal = Signal()
@receiver(signal, sender="main")
def my_receiver(sender, **kwargs):
print("here is my receiver.")
print("hello sender: {}".format(sender))
if __name__ == "__main__":
print("begin...")
signal.send(sender="main")
输出:
代码语言:javascript复制begin...
here is my receiver.
hello sender: main
Django 内置的 signal
Django 中内置了很多 个 signal ,方便我们直接使用。
模型相关
代码语言:javascript复制pre_init = ModelSignal(providing_args=["instance", "args", "kwargs"], use_caching=True) # 对象初始化前
post_init = ModelSignal(providing_args=["instance"], use_caching=True) #对象初始化后
pre_save = ModelSignal(providing_args=["instance", "raw", "using", "update_fields"],
use_caching=True) # 对象保存修改前
post_save = ModelSignal(providing_args=["instance", "raw", "created", "using", "update_fields"], use_caching=True) #对象保存修改后
pre_delete = ModelSignal(providing_args=["instance", "using"], use_caching=True) #对象删除前
post_delete = ModelSignal(providing_args=["instance", "using"], use_caching=True) #对象删除后
m2m_changed = ModelSignal(
providing_args=["action", "instance", "reverse", "model", "pk_set", "using"],
use_caching=True,
) #ManyToManyField 字段更新后触发
pre_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using", "apps", "plan"]) #数据迁移前
post_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using", "apps", "plan"]) #数据迁移后
请求相关
代码语言:javascript复制request_started = Signal(providing_args=["environ"]) #request 请求前
request_finished = Signal() #request 请求后
got_request_exception = Signal(providing_args=["request"]) #request 请求出错后
setting_changed = Signal(providing_args=["setting", "value", "enter"]) #request 请求某些设置被修改后