如何用C++ 写Python模块扩展(二)

Python模块包含的类创建(下)

  1. 类的方法表创建

    • 直接上代码

         static PyMethodDef VCam_MethodMembers[] =      //类的所有成员函数结构列表同样是以全NULL结构结束
        {
            { "set_fill", (PyCFunction)VCam_SetFill, METH_VARARGS, "Set video resize method (0: Aspect fit, 1: Aspect fill, 2: Stretch), used when input frame size differs from VCam output size." },
            { "mirror", (PyCFunction)VCam_Mirror, METH_VARARGS, "Mirror the output video (0: no mirror, others: mirror), non-persistent." },
            { "rotate", (PyCFunction)VCam_Rotate, METH_VARARGS, "Rotate the input video 90 degree (0: no rotate, others: rotate), non-persistent." },
            { "flip", (PyCFunction)VCam_Flip, METH_VARARGS, "Vertical flip the output video(0: no flip, others : flip), non - persistent." },  
            { "send_image", (PyCFunction)VCam_SendImg, METH_VARARGS, "Display a image( path) to vCam." },
            { "capture_screen", (PyCFunction)VCam_CaptureScreen, METH_VARARGS, "Capture region of screen and set it as VCam output." },
            { NULL, NULL, NULL, NULL }
        };
      
    • PyMethondDef 结构的定义

        struct PyMethodDef {
            const char  *ml_name;   /* The name of the built-in function/method */
            PyCFunction ml_meth;    /* The C function that <isindex></isindex>mplements it */
            int         ml_flags;   /* Combination of METH_xxx flags, which mostly
                                       describe the args expected by the C func */
            const char  *ml_doc;    /* The __doc__ attribute, or NULL */
        };
        typedef struct PyMethodDef PyMethodDef;
      
        #define PyCFunction_New(ML, SELF) PyCFunction_NewEx((ML), (SELF), NULL)
        PyAPI_FUNC(PyObject *) PyCFunction_NewEx(PyMethodDef *, PyObject *,
                                                 PyObject *);
      
        /* Flag passed to newmethodobject */
        /* #define METH_OLDARGS  0x0000   -- unsupported now */
        #define METH_VARARGS  0x0001
        #define METH_KEYWORDS 0x0002
        /* METH_NOARGS and METH_O must not be combined with the flags above. */
        #define METH_NOARGS   0x0004
        #define METH_O        0x0008
      
        /* METH_CLASS and METH_STATIC are a little different; these control
           the construction of methods for a class.  These cannot be used for
           functions in modules. */
        #define METH_CLASS    0x0010
        #define METH_STATIC   0x0020
      
        /* METH_COEXIST allows a method to be entered even though a slot has
           already filled the entry.  When defined, the flag allows a separate
           method, "__contains__" for example, to coexist with a defined
           slot like sq_contains. */
      
        #define METH_COEXIST   0x0040
      
    • 其他没啥好说的结构定义已经很明白了,就是第三个元素ml_falg 需要根据函数时机传入参数要求进行调整 就说几个常用的flag 其他见手册

      • METH_NOARGS 表示没有参数传入,
      • METH_KEYWORDS 表示传入keyword参数
      • METH_VARARGS 表示传入位置参数
      • 部分flag可以组合传入如 METH_VARARGS|METH_KEYWORDS
      • 注意METH_NOARGS 不能与 前面两个flag组合使用
  2. 写类的内置属性信息表说明PyTypeObject实例VCam_ClassInfo

    • 直接上代码,代码中包含了PyTypeObject结构体 大部分元素,具体见object.h 头文件定义

        static PyTypeObject VCam_ClassInfo =
        {
            PyVarObject_HEAD_INIT(NULL, 0)
            "PyVcam.VCam",            //可以通过__class__获得这个字符串. CPP可以用类.__name__获取.   const char *
            sizeof(VCam),                 // tp_basicsize 类/结构的长度.调用PyObject_New时需要知道其大小.  Py_ssize_t
            0,                              //tp_itemsize  Py_ssize_t
            (destructor)VCam_Destruct,    //类的析构函数.      destructor
            0,                            //类的print 函数      printfunc
            0,                               //类的getattr 函数  getattrfunc
            0,                              //类的setattr 函数   setattrfunc
            0,                              //formerly known as tp_compare(Python 2) or tp_reserved (Python 3)  PyAsyncMethods *
            0,          //tp_repr 内置函数调用。    reprfunc
            0,                              //tp_as_number   指针   PyNumberMethods *
            0,                              //tp_as_sequence 指针   PySequenceMethods *
            0,                              // tp_as_mapping 指针  PyMappingMethods *
            0,                              // tp_hash   hashfunc
            0,                              //tp_call     ternaryfunc
            0,          //tp_str/print内置函数调用.   reprfunc
            0,                          //tp_getattro    getattrofunc
            0,                          //tp_setattro     setattrofunc
            0,                          //tp_as_buffer 指针 Functions to access object as input/output buffer   PyBufferProcs
            Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,                 //tp_flags    如果没有提供方法的话,为Py_TPFLAGS_DEFAULE     unsigned long
            "VCam Module write by C++!",                   // tp_doc  __doc__,类/结构的DocString.  const char *
            0,                                                   //tp_traverse    call function for all accessible objects    traverseproc
            0,                                                      // tp_clear   delete references to contained objects    inquiry
            0,                                                      //  tp_richcompare   richcmpfunc
            0,                                                      //tp_weaklistoffset  Py_ssize_t
            0,                                                      // tp_iter  getiterfunc
            0,                                          //tp_iternext    iternextfunc
      
      
            /* Attribute descriptor and subclassing stuff */
      
            VCam_MethodMembers,        //类的所有方法集合.   PyMethodDef *
            VCam_DataMembers,          //类的所有数据成员集合.  PyMemberDef *
            0,                                              // tp_getset   PyGetSetDef *
            0,                                              //  tp_base   _typeobject *
            0,                                              //  tp_dict     PyObject *
            0,                                              // tp_descr_get   descrgetfunc
            0,                                                          //tp_descr_set   descrsetfunc
            0,                                              //tp_dictoffset   Py_ssize_t
            (initproc)VCam_init,      //类的构造函数.tp_init       initproc
            0,                      //tp_alloc  allocfunc
            0,                          //tp_new   newfunc
            0,                        // tp_free    freefunc
            0,                      //  tp_is_gc     inquiry
        };
      
    • VCam_ClassInfo 中把前面所创建的 init函数、析构函数、方法表、成员表等加入类信息表

模块创建和初始化

  1. 创建模块信息

    • 直接上代码

        static PyModuleDef ModuleInfo =
         {
         PyModuleDef_HEAD_INIT,
         "PyVcam",               //模块的内置名--__name__.
         NULL,                 //模块的DocString.__doc__
         -1,
         NULL, NULL, NULL, NULL, NULL
         };
      
    • PyModuleDef 结构题定义

        typedef struct PyModuleDef{
          PyModuleDef_Base m_base;
          const char* m_name;
          const char* m_doc;
          Py_ssize_t m_size;
          PyMethodDef *m_methods;
          struct PyModuleDef_Slot* m_slots;
          traverseproc m_traverse;
          inquiry m_clear;
          freefunc m_free;
        } PyModuleDef;
      
  2. 初始化模块

    • 先上代码

        PyMODINIT_FUNC PyInit_PyVcam(void)       //模块外部名称为--PyVcam
        {
            
            Gdiplus::GdiplusStartupInput StartupInput;
            GdiplusStartup(&m_gdiplusToken, &StartupInput, NULL);
      
            PyObject* pReturn = 0;
            VCam_ClassInfo.tp_new = PyType_GenericNew;       //此类的new内置函数—建立对象.
            
            
            if (PyType_Ready(&VCam_ClassInfo) < 0)
                return NULL;
      
            pReturn = PyModule_Create(&ModuleInfo);
            if (pReturn == NULL)
                return NULL;
            Py_INCREF(&VCam_ClassInfo);
            PyModule_AddObject(pReturn, "VCam", (PyObject*)&VCam_ClassInfo); //将这个类加入到模块的Dictionary中.
            
            return pReturn;
        }
      
    • 代码解释:

      • Python模块必须要导出一个返回值为PyObject*名为PyInit_XXX的函数用来初始化模块信息,Python加载模块时候回去直接调用此函数来初始化。
      • PyMODINIT_FUNC 宏其实就是以下语句: __declspec(dllexport) PyObject*
      • VCam_ClassInfo.tp_new = PyType_GenericNew; 这条语句其实可以不用写直接在前面VCam_Classinfo里面对应位置加入PyType_GenericNew 即可,想想找到对应那个置要找到眼花 干脆直接以这种形式写出来;反之前面整个VCam_Classinfo 后面的结构其实可以不写,直接以VCam_Classinfo.xxx =xxx的形式写出
      • 调用一个PyType_Ready (&VCam_ClassInfo)来完成类的定义
      • 然后用PyModule_Create(&ModuleInfo) 创建Module
      • 调用PyModule_AddObject将 Vcam类加入到Module 中 同时别忘了增加类体引用计数
      • 将模块返回给Python 大功告成

PythonC扩展的执行效率问题(GIL)

  1. GIL问题
    • GIL锁原理

        for (;;) {
            if (--ticker < 0) {   //这是之前版本的GIL锁原理 执行check_interval条数指令 放一次GIL 貌似现在版本不再按照指令条数来放锁了而是按照时间间隔
                ticker = check_interval;             
                /* Give another thread a chance */
                PyThread_release_lock(interpreter_lock);   //释放 GIL         
                /* Other threads may run now */             
                PyThread_acquire_lock(interpreter_lock, 1); //立马重新申请GIL 一放一抢 其他线程就有机会
            }             
            bytecode = *next_instr++;  //这里读入python指令
            switch (bytecode) {  
                /* execute the next instruction ... */  //执行指令
            }
        }
      
    • 由于CPython GIL存在在进行多线程任务时 python指令在执行时会一直占着GIL导致其他线程一直在等着抢锁 于是多线程就编程了单线程,无论你开多少个线程貌似都只能同时有一个线程在运行

  2. GIL锁问题的解决
    在纯Python环境下CPython的GIL貌似无解了,但是GIL真的无解了么?
    • 大家都知道IO密集型场景利用多线程能显著提高执行效率,也就是说IO任务执行过程中释放了GIL 显然这个释放肯定不是在ticker<0时释放的, IO任务到底是怎么释放GIL的呢

      • IO任务释放原理如下

          /* s.connect((host, port)) method */
          static PyObject *
          sock_connect(PySocketSockObject *s, PyObject *addro)
          {
              sock_addr_t addrbuf;
              int addrlen;
              int res;
           
              /* convert (host, port) tuple to C address */
              getsockaddrarg(s, addro, SAS2SA(&addrbuf), &addrlen);
           
              Py_BEGIN_ALLOW_THREADS
              res = connect(s->sock_fd, addr, addrlen);
              Py_END_ALLOW_THREADS
           
              /* error handling and so on .... */
          }
        
      • 上面是部分socket代码,可以看到在执行 connect之前 调用了一个宏 Py_BEGIN_ALLOW_THREADS 这个宏就是用来释放GIL的 成功connect后又调用 Py_END_ALLOW_THREADS重新申请GIL

      • 解决Python多线程运行效率问题似乎有门

    • Python计算密集型不能用多线程?似乎利用C++写一个模块来处理计算任务多线程照样能达到并行效果

      • 下面就开始写代码验证这个问题
        • 在c++模块中写了两个计算密集型函数,函数计算返回之类的算法都没有区别唯一区别就是: 其中一个函数在高密度计算前释放了GIL计算完成后重新申请锁

          static PyObject* Gil_free(GilTest* self,PyObject* args){    
             LONGLONG num;
             if (!PyArg_ParseTuple(args, "L", &num))return NULL;
             
             LONGLONG rst;
             Py_BEGIN_ALLOW_THREADS
                 for (LONGLONG i = 1; i <= num * 100; i++)
                 {
                     for (LONGLONG j = 1; j <= num * 100; j++)
                     {
                         rst = i*j;
                     }
                 }                
             Py_END_ALLOW_THREADS
                 return Py_BuildValue("i", rst);
          

          }
          static PyObject* Gil_lock(GilTest* self, PyObject* args){
          LONGLONG num;
          if (!PyArg_ParseTuple(args, "L", &num))return NULL;
          LONGLONG rst;
          for (LONGLONG i = 1; i <= num * 100; i++)
          {
          for (LONGLONG j = 1; j <= num * 100; j++)
          {
          rst = i*j;
          }
          }
          return Py_BuildValue("i", rst);
          }

      • 将函数封装到一个python模块中调用模块 写一个脚本开多线程执行

          from GilTest import GilTest
          import time
          from threading import Thread
        
        
          def foo(num, i, start):
              obj = GilTest()
              obj.compute_with_gil(num)   # 调用的函数计算时没有释放GIL
              print("foo %s is over" % i, time.time() - start)
        
        
          def bar(num, i, start):
              obj = GilTest()
              obj.compute_without_gil(num)   # 调用的函数计算时释放GIL
              print("bar %s is over" % i, time.time() - start)
        
        
          def run():
              print("stat foo")
              start = time.time()  # 开foo线程开始计时
              thread_list1 = []
              for i in range(10):
                  thread_list1.append(Thread(target=foo, args=(1000, i, start)))
              for i in thread_list1:
                  i.start()
              for i in thread_list1:
                  i.join()
              print("stat bar")
              time.sleep(1) 
              start = time.time()   # 开bar线程开始计时
              thread_list2 = []
              for i in range(10):
                  thread_list2.append(Thread(target=bar, args=(1000, i, start)))
              for i in thread_list2:
                  i.start()
              for i in thread_list2:
                  i.join()
        
        
          if __name__ == '__main__':
        
              run()
        
      • 输出执行结果

          stat foo
          foo 0 is over 2.2932560443878174
          foo 1 is over 4.577575445175171
          foo 2 is over 6.859208583831787
          foo 3 is over 9.145148277282715
          foo 4 is over 11.43115520477295
          foo 5 is over 13.71883225440979
          foo 6 is over 15.999829292297363
          foo 7 is over 18.281397581100464
          foo 8 is over 20.57776975631714
          foo 9 is over 22.851707935333252
          stat bar
          bar 3 is over 4.594241380691528
          bar 6 is over 4.594241380691528
          bar 7 is over 4.609868288040161
          bar 2 is over 4.63910174369812
          bar 8 is over 5.750362157821655
          bar 4 is over 5.765988826751709
          bar 5 is over 5.859748840332031
          bar 0 is over 5.859748840332031
          bar 1 is over 5.859748840332031
          bar 9 is over 5.937881946563721
        
          Process finished with exit code 0
        
      • 可以发现foo线程完全像是在运行单线程,每个线程执行完成时间比上一个线程大约多2.3秒看;而bar线程是真正的多线程 ,线程完成计算时间差别很小,而且完成先后顺序是乱序的,因为CPU是四核的所以线程之间还是会存在抢cpu情况,每个线程运行时间较foo要长一点(foo每个线程的运算几乎是独占运行)

      • 再来看看CPU占用

      在执行foo时python进程占用CPU约15%作用,当程序执行到bar线程时可以看到python进程cpu占用直线上飙到接近100%的占用,这也说明了此时python的线程是并行的。

  3. 释放GIL的注意事项
    前面演示似乎解决python多线程GIL问题,执行效率很高,是不是意味着不论啥玩意拿过来直接释放GIL就好了呢,显然不可能是这样的,考虑下GIL为啥存在吧,在c++中释放GIL需要考虑以下问题:
    • 首先要预估函数执行需要多少时间(需要几个周期),若代码几部就能走完那么如果此时释放GIL不但不能提高效率反而会降低代码执行效率,因为释放和申请GIL也是有系统开销的
    • 如果C++代码存在读取或操作PyObject的代码那么在执行这些代码时必须确保线程持有GIL锁
    • 若在函数内释放了GIL锁函数返回前必须重新申请GIL锁,否则将带来灾难性后果

如何用C++ 写Python模块扩展(一)
模块C++源码下载
好了先写到这请期待后续的大招 封装windows系统的com组件到python模块

原文地址:https://www.cnblogs.com/RexShao/p/8453032.html