如何用C++ 写Python模块扩展(二)
Python模块包含的类创建(下)
-
类的方法表创建
-
直接上代码
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组合使用
-
-
写类的内置属性信息表说明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函数、析构函数、方法表、成员表等加入类信息表
-
模块创建和初始化
-
创建模块信息
-
直接上代码
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;
-
-
初始化模块
-
先上代码
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)
- 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导致其他线程一直在等着抢锁 于是多线程就编程了单线程,无论你开多少个线程貌似都只能同时有一个线程在运行
-
- 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的线程是并行的。
- 下面就开始写代码验证这个问题
-
- 释放GIL的注意事项
前面演示似乎解决python多线程GIL问题,执行效率很高,是不是意味着不论啥玩意拿过来直接释放GIL就好了呢,显然不可能是这样的,考虑下GIL为啥存在吧,在c++中释放GIL需要考虑以下问题:- 首先要预估函数执行需要多少时间(需要几个周期),若代码几部就能走完那么如果此时释放GIL不但不能提高效率反而会降低代码执行效率,因为释放和申请GIL也是有系统开销的
- 如果C++代码存在读取或操作PyObject的代码那么在执行这些代码时必须确保线程持有GIL锁
- 若在函数内释放了GIL锁函数返回前必须重新申请GIL锁,否则将带来灾难性后果
如何用C++ 写Python模块扩展(一)
模块C++源码下载
好了先写到这请期待后续的大招 封装windows系统的com组件到python模块