Django 的信号机制

2018/07/30 22:20 下午 posted in  Django

Django 将 signal 描述为“信号调度员”,主要以信号的形式,来触发多个应用程序。这篇文章将从源码分析的角度,讲解 Django 中 signal 的工作机制及使用方法。

Signal 类

blog.cdn.updev.cn
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 中对垃圾回收的处理采用的是标记引用的方式,而弱引用的作用在于避免循环引用导致内存泄漏。主要原理是在弱引用某对象时,不在引用标记中增加引用数,所以在该对象的强引用为 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 表示是否用弱引用,默认为 Truereceiver 可以是函数,也可以是方法,而 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")

输出:

begin...
here is my receiver.
hello sender: main

Django 内置的 signal

Django 中内置了很多 个 signal ,方便我们直接使用。

模型相关

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"])  #数据迁移后

请求相关

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 请求某些设置被修改后