有比较成熟的c++ rpc框架,想要基于python代码快速搭建一个线上可用的rpc服务时,可用用c api的方式调用python脚本,将其封装成一个rpc服务。
这种方式的优点是相比将python代码翻译成tensorflow/pytorc的c++ 模块调用,开发成本要小很多;缺点是性能较低,只能做到单个并发,只适合于有成熟c++ rpc框架且无python版rpc框架的情况。
开发过程中有一些要注意的问题,在此记录一下。

初始化

embedding-python文档

初始化,python解释器在同一个线程中可以初始化多次,能正常返回,但是在不同线程初始化的适合,整个程序会直接core掉。另外初始化的时候有一套解释器拉起/so库选择的规则,在文档中有描述,使用的时候要格外注意。

全局锁

python c api线程安全文档

多线程调用c api封装的函数时,需要加全局锁,否则可能会导致程序挂掉。

参数传递

c api args文档

标注库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 c api数据结构描述文档

可参考此文档解析从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;
}

引用计数

python c api引用计数文档

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并行计算。

☞ 参与评论