有比较成熟的c++ rpc框架,想要基于python代码快速搭建一个线上可用的rpc服务时,可用用c api的方式调用python脚本,将其封装成一个rpc服务。
这种方式的优点是相比将python代码翻译成tensorflow/pytorc的c++ 模块调用,开发成本要小很多;缺点是性能较低,只能做到单个并发,只适合于有成熟c++ rpc框架且无python版rpc框架的情况。
开发过程中有一些要注意的问题,在此记录一下。
初始化
初始化,python解释器在同一个线程中可以初始化多次,能正常返回,但是在不同线程初始化的适合,整个程序会直接core掉。另外初始化的时候有一套解释器拉起/so库选择的规则,在文档中有描述,使用的时候要格外注意。
全局锁
多线程调用c api封装的函数时,需要加全局锁,否则可能会导致程序挂掉。
参数传递
标注库string的传递,Py_BuildValue会根据\0自动判断c str的结尾,并传递给python
PyObject *pInitArgs = Py_BuildValue("(s)", _sModelPath.c_str());
二进制数据块的传递,此时注意标识不再是s了,而是s#,这样Py_BuildValue就不会再根据\0判断数据结束,而是根据后面传入的数据长度来判断。
PyObject *pArgs = Py_BuildValue("(s#)", c, audioBuf.size());
传递二进制数据加上别的格式的数据,将别的数据依次往后加就行了
PyObject *pArgs = Py_BuildValue("(s#,i)", c, audioBuf.size(), iAudioFormat);
结果解析
可参考此文档解析从python返回的数据结构,如解析元组类型:
if(pRet && PyTuple_Check(pRet) && PyTuple_Size(pRet) == 2){
PyObject* a = PyTuple_GetItem(pRet, 0);
PyObject* b = PyTuple_GetItem(pRet, 1);
PyArg_Parse( a, "i", &c );
PyArg_Parse( b, "i", &d);
} else {
c = -1;
d = -1;
}
引用计数
Py_Object是所有py c类型的基类,它保存了一个指向python对象的指针和对象的引用计数,在使用完后要调用一下Py_DECREF/Py_XDECREF来减一个引用计数,引用计数到0时会调用python对象的析构函数。其中Py_DECREF需要明确指针非null,而Py_XDECREF则无所谓。
cnn前向运算不能跨线程
这里遇到了一个奇怪的问题,python中cnn的前向运算无法跨线程,最开始试了TensorFlow,后来换成PyTorch也是一样。
具体表现是,在a线程中初始化python解释器,然后:
- 在a线程或者b线程中初始化tf/pytorch,加载模型,可以成功
- 在a线程调用前向运算的函数,可以成功
- 在b线程调用前向运算的函数,会一直卡在函数调用的位置,不抛异常,也不返回
- 在a/b线程中调用除了前向运算之外的其它函数,都可以成功
原因暂时没有去探究,先记录一下现象。所以这种做法只适合用来快速搭建demo。真正实现高性能服务的话还是要使用c++版本的tensorflow或者pytorch,来多线程调用,或者是run batch并行计算。
本文链接:https://www.zoucz.com/blog/2020/11/03/c1e17640-1da5-11eb-90b5-eb40e9720ed0/