zoukankan      html  css  js  c++  java
  • 关于不能对闭包函数进行热更新的问题

    目前项目组正在使用的热更新机制有一些潜规则,其中一个就是不能更新闭包函数(因此也就不能对函数使用装饰器修饰)。

    热更新机制原理

    先来说说目前的热更新机制的原理,由于更新类是一个较为复杂的话题,因此这里只讨论更新函数的情况。

    当需要热更新一个函数时:

    (1)首先是调用python的built-in函数reload,这个函数会把模块重编并重新执行。

    (2)然后再找出所有引用了旧函数的地方,将其替换为引用新的函数。

    复杂的地方在于第二个步骤,如何做到更新所有的引用呢?看看python里面函数的实现:

    typedef struct {
        PyObject_HEAD
        PyObject *func_code;    /* A code object */
        PyObject *func_globals;    /* A dictionary (other mappings won't do) */
        PyObject *func_defaults;    /* NULL or a tuple */
        PyObject *func_closure;    /* NULL or a tuple of cell objects */
        PyObject *func_doc;        /* The __doc__ attribute, can be anything */
        PyObject *func_name;    /* The __name__ attribute, a string object */
        PyObject *func_dict;    /* The __dict__ attribute, a dict or NULL */
        PyObject *func_weakreflist;    /* List of weak references */
        PyObject *func_module;    /* The __module__ attribute, can be anything */
    
        /* Invariant:
         *     func_closure contains the bindings for func_code->co_freevars, so
         *     PyTuple_Size(func_closure) == PyCode_GetNumFree(func_code)
         *     (func_closure may be NULL if PyCode_GetNumFree(func_code) == 0).
         */
    } PyFunctionObject;

    python的函数也是对象,并对应于c中PyFunctionObject结构体。因此这里有一个取巧的做法,只需要将PyFunctionObject结构体中的成员替换更新即可。

    这个做法简单方便、易于实现,并且很多成员的替换可以在python层实现。现在项目组的热更新模块就是这样做的。

    def update_function(old_fun, new_fun):
        #更新函数的PyCodeObject
        old_fun.func_code = new_fun.func_code
        #更新其它
        ...

    python中闭包的实现

    先来简单的了解一下python中闭包的实现。

    def dec(f):
        def warp():
            f()
        return warp

    函数dec编译后的字节码如下:

      3           0 LOAD_CLOSURE             0 (f)
                  3 BUILD_TUPLE              1
                  6 LOAD_CONST               1 (<code object warp at 0000000002F71A30, file "test.py", line 3>)
                  9 MAKE_CLOSURE             0
                 12 STORE_FAST               1 (warp)
    
      5          15 LOAD_FAST                1 (warp)
                 18 RETURN_VALUE

    可以看到dec函数会将被内层函数warp引用到的对象(如f)包装成一个cellobject,再打包成一个tuple传递给warp。在虚拟机执行MAKE_CLOSURE指令时会通过PyFunction_SetClosure函数将这个tuple设置到PyFunctionObject结构的func_closure成员上。

    为什么现在的热更新模块不支持更新闭包函数?

    在了解到闭包实现之后,我们知道了在PyFunctionObject结构体上面有个成员func_closure,里面会引用住一些闭包会使用到的函数(如warp的f)。如果对函数warp热更时不替换这部分的数据,那么更新之后函数还是引用了旧的f函数!目前项目组的热更模块就是缺少对func_closure的替换。好了找到问题所在了,接下来的问题就是如何更新func_closure成员。

    更新func_closure的第一次尝试

    更新func_closure最直观的想法应该是这样的:

    def update_function(old_fun, new_fun):
        #更新函数的PyCodeObject
        old_fun.func_code = new_fun.func_code
        #更新闭包数据
        old_fun.func_closure = new_fun.func_closure
        #更新其它
        ...

    可惜不行,func_closure是一个readonly property。

    更新func_closure的第二次尝试

    既然不行那我遍历tuple,更新其中的cellobject总可以了吧。遗憾的是也不行,cellobject对象身上的cell_contents是不可写的(详情参考CPython源码中的cellobject.c),代码就不放上来了。

    更新func_closure的第三次尝试

    这一次我是决定直接在c里面改这个指针,这样基本可以绕过python对其的限制。具体方式是用c实现一个扩展模块:

    //Note Since Python may define some pre-processor definitions 
    //which affect the standard headers on some systems, 
    //you must include Python.h before any standard headers are included.
    #include "Python.h"
    
    static PyObject *
    PyReload_UpdateFunctionClosure(PyObject *self, PyObject *args) {
        PyObject *o1, *o2;
    
        if (!PyArg_ParseTuple(args, "OO", &o1, &o2)) {
            return NULL;
        }
        if (!PyFunction_Check(o1) || !PyFunction_Check(o2)) {
            return NULL;
        }
        PyObject* closure = PyFunction_GetClosure(o2);
        if (closure == NULL) {
            return NULL;
        }
        if (PyFunction_SetClosure(o1, closure) != 0) {
            return NULL;
        }
        Py_RETURN_NONE;
    }
    
    static PyMethodDef PyReload_Methods[] =
    {
        { "update_function_closure",  PyReload_UpdateFunctionClosure, METH_VARARGS, "更新python函数闭包数据" },
        { NULL, NULL, 0, NULL }/* Sentinel */
    };
    
    PyMODINIT_FUNC
    initPyReload(void)
    {
        (void)Py_InitModule("PyReload", PyReload_Methods);
    }

    在python里面的更新函数可以这么写:

    def update_function(old_fun, new_fun):
        #更新函数的PyCodeObject
        old_fun.func_code = new_fun.func_code
        #更新闭包数据
        if new_fun.func_closure:
            import PyReload
            PyReload.update_function_closure(old_fun, new_fun)   

    这样总算可以了。不过需要注意的是要正确处理好引用计数问题,还有就是不知道这段几十行的代码还有无别的问题。毕竟都千方百计不让你对func_closure进行修改了,或许这里面有坑,并且我没有注意到:)

  • 相关阅读:
    HDU 1874 畅通工程续(dijkstra)
    HDU 2112 HDU Today (map函数,dijkstra最短路径)
    HDU 2680 Choose the best route(dijkstra)
    HDU 2066 一个人的旅行(最短路径,dijkstra)
    关于测评机,编译器,我有些话想说
    测评机的优化问题 时间控制
    CF Round410 D. Mike and distribution
    数字三角形2 (取模)
    CF Round410 C. Mike and gcd problem
    CF Round 423 D. High Load 星图(最优最简构建)
  • 原文地址:https://www.cnblogs.com/adinosaur/p/7710393.html
Copyright © 2011-2022 走看看