最近都很忙,忙着把公司的Python回测框架完成。前两天公司同事抱怨 C/C++调用Python超级烦人,动不动就返回NULL,而且内存暴涨,于是我决定尝试解决这个问题,提供一套完整的开发流程,供大家技术分享。要完成C/C++调用Python最好是熟悉C/C++和Python,否则出了问题就比较难解决。
-
- Visual Studio / Python 环境搭建
- C++调用Python的接口示例
- C++多线程调用嵌入Python
- 启用线程支持
- 线程状态与全局解释器锁(GIL)
- 从扩展代码执行释放GIL
- 非Python创建的线程
- 思考
- 加好友 & 工程下载
之前没有使用过 C/C++ 调用嵌入Python,只用过 Cython 编写Python 扩展程序。基本是从零开始学习,但我并不想快速完成任务,否则随便度娘一下就OK,事实上,那样做虽能快速解决问题,但只知其一不知其二还是比较心虚。于是从官方文档开始看起。
Visual Studio / Python 环境搭建
在各大操作系统上安装Python时,同时安装了 C++开发资源,包括: Include文件,静态链接库文件,动态链接库文件。标准的官方文档包含了 Python/C API 以及 Extending and Embedding 主题文档。
VS2015+Python3.6.1, 我直接用我所建立的工程来讲解,请根据自己实际情况修改。
- 官网下载安装 Python3 相应版本
- 官网下载 python-3.6.1-embed-amd64.zip 文件
解压之后拷贝到工程生成exe所在目录, 注意python.exe 与生成exe目录同级。 - VS新建项目, 设置项目 Python 头文件路径
配置属性>C/C++>常规>附加包含目录你的Python安装目录\include
, 比如我的:
D:\CodeTool\Python\Python36\include
- 复制 python36.lib 到 cpp 文件所在目录,设置项目属性方式设置 lib 路径
D:\CodeTool\Python\Python36\libs\python36.lib
- 修改 pyconfig.h 文件,Debug 工程不会提示找不到
python36_d.lib
line 337 左右, 增加 //
#ifdef _DEBUG
//# define Py_DEBUG
#endif
line 292 左右 ,修改 python36_d.lib
# if defined(_DEBUG)
# pragma comment(lib,"python36.lib")
//# pragma comment(lib,"python36_d.lib")
# elif defined(Py_LIMITED_API)
# pragma comment(lib,"python3.lib")
# else
# pragma comment(lib,"python36.lib")
# endif /* _DEBUG */
C++调用Python的接口示例
test1.cpp
通过 #pragma comment
指令引入 lib 库
#include <Python.h>
#pragma comment(lib, "python36.lib")
void test_use_multi_param()
{
PyObject *use_int, *use_str, *use_byte, *use_tuple;
PyObject *use_list, *use_dict, *use_complex;
PyObject *pName, *pModule, *pFunc;
PyObject *pArgs, *pValue;
const char* module_name = "multiply";
const char* module_method = "test_use_mulit_params";
Py_Initialize();
use_int = Py_BuildValue("i", 123);
use_str = Py_BuildValue("s", "hello");
use_byte = Py_BuildValue("y", "hello2");
use_tuple = Py_BuildValue("(iis)", 1, 2, "three");
use_list = Py_BuildValue("[iis]", 1, 2, "three");
use_dict = Py_BuildValue("{s:i,s:i}", "abc", 123, "def", 456);
use_complex = Py_BuildValue("[ii{ii}(is){s:i}]", 1,2,3,4,5,"xcxcv","ff",1);
pName = PyUnicode_DecodeFSDefault(module_name);
pModule = PyImport_Import(pName);
Py_DECREF(pName);
if (pModule != NULL)
{
pFunc = PyObject_GetAttrString(pModule, module_method);
if (pFunc && PyCallable_Check(pFunc))
{
pArgs = PyTuple_New(7);
PyTuple_SetItem(pArgs, 0, use_int);
PyTuple_SetItem(pArgs, 1, use_str);
PyTuple_SetItem(pArgs, 2, use_byte);
PyTuple_SetItem(pArgs, 3, use_list);
PyTuple_SetItem(pArgs, 4, use_tuple);
PyTuple_SetItem(pArgs, 5, use_dict);
PyTuple_SetItem(pArgs, 6, use_complex);
pValue = PyObject_CallObject(pFunc, pArgs);
Py_DECREF(pArgs);
if (pValue != NULL)
{
int ret_int;
char *ret_str, *ret_byte;
PyObject* ret_list, *ret_tuple, *ret_dict, *ret_complex;
//解析元组
PyArg_ParseTuple(pValue, "isyOOOO", &ret_int, &ret_str, &ret_byte, &ret_list,&ret_tuple,&ret_dict,&ret_complex);
Py_DECREF(pValue);
}
else {
Py_DECREF(pFunc);
Py_DECREF(pModule);
PyErr_Print();
fprintf(stderr, "Call failed\n");
}
}
else
{
if (PyErr_Occurred())
PyErr_Print();
fprintf(stderr, "Cannot find function \"%s\"\n", module_method);
}
Py_XDECREF(pFunc);
Py_DECREF(pModule);
}
else
{
PyErr_Print();
fprintf(stderr, "Failed to load \"%s\"\n", module_name);
}
Py_FinalizeEx();
}
int main(int argc, char *argv[])
{
test_use_multi_param();
system("pause");
}
这是一个调用 Python 函数的基本用法,其中包含了几个阶段:
- Py_Initialize - Py_FinalizeEx
- Py模块加载,Py函数加载,Py函数参数构造,调用Py函数,获取Py函数返回,
- 变量引用计数处理/ 错误处理
变量引用计数管理,请直接参考 引用计数
C/C++ 使用Python对象,对于引用计数一定要如履薄冰,否则就会出现内存泄漏。
C++多线程调用嵌入Python
在我们公司里,C++程序会运行嵌入Pyhton作为扩展接口。在C++多线程环境下,直接调用 api操作 Python解释器,肯定会导致 core dump
, 因为 Python 绝大部分函数都是非线程安全的。由GIL控制访问顺序。
启用线程支持
Py_Initialize();
PyEval_InitThreads();
// 其它代码
Py_FinalizeEx();
编译解释器库时启用了多线程支持(VS默认支持),才能使用 PyEval_InitThreads, 如果你的程序不需要多线程,那么建议关闭多线程支持。
线程状态与全局解释器锁(GIL)
Python解释器不是完全线程安全的。为了支持多线程Python程序,有一个全局锁,称为 global interpreter lock or GIL,在当前线程能够安全访问Python对象之前,它必须由当前线程持有。没有锁,即使是最简单的操作也可能导致多线程程序中的问题:例如,当两个线程同时增加相同对象的引用计数时,引用计数可能最终只增加一次,而不是增加两次。
因此,存在这样的规则,即只有获取了GIL的线程可以操作Python对象或调用Python/C API函数。为了模拟执行的并发性,解释程序经常尝试切换线程(参见sys.setswitchinterval())。该锁还围绕可能阻塞I/O操作(如读取或写入文件)释放,以便其他Python线程可以同时运行。
Python解释器将一些特定于线程的簿记信息保存在称为PyThreadState
的数据结构内。还有一个全局变量指向当前的PyThreadState
状态:它可以使用PyThreadState_Get()
检索。
参考自:https://docs.python/3/c-api/init.html
从扩展代码执行释放GIL
Py_BEGIN_ALLOW_THREADS
... Do some blocking I/O operation ...
Py_END_ALLOW_THREADS
以上宏实际展开
PyThreadState *_save
_save = PyEval_SaveThread()
...Do some blocking I/O operation...
PyEval_RestoreThread(_save)
非Python创建的线程
如果需要从第三方即非Python创建线程调用Python代码(通常这将是上述第三方库提供的回调API的一部分),则必须首先通过创建线程状态数据结构来向解释器注册这些线程,然后获取GIL,最后存储它们的线程状态指针,然后可以开始使用Python /C API。完成后,您应该重置线程状态指针,释放GIL,并最终释放线程状态数据结构。
PyGILState_Ensure()和PyGILState_Release()函数自动执行上述所有操作。从C线程调用Python的典型习惯用法是:
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();
/* Perform Python actions here. */
result = CallSomeFunction();
/* evaluate result or handle exception */
/* Release the thread. No Python API allowed beyond this point. */
PyGILState_Release(gstate);
注意:PyGILState_xx()函数假设只有一个全局解释器(由Py_Initialize()自动创建)。Python支持创建额外的解释器(使用Py_NewInterpreter()),但不支持混合多个解释器和PyGILState_xx() API。
参考自:https://docs.python/3/c-api/init.html
根据上面官方文档,就可以轻易写出相关代码了。
// 封装PyGILState_Ensure/PyGILState_Release
class PythonThreadLocker
{
PyGILState_STATE state;
public:
PythonThreadLocker() : state(PyGILState_Ensure())
{}
~PythonThreadLocker() {
PyGILState_Release(state);
}
};
int CallSomeFunction()
{
int argc = 5;
char *argv[] = { "", "multiply", "multiply", "3", "2" };
PyObject *pName, *pModule, *pFunc;
PyObject *pArgs, *pValue;
int i;
pName = PyUnicode_DecodeFSDefault(argv[1]);
/* Error checking of pName left out */
pModule = PyImport_Import(pName);
Py_DECREF(pName);
if (pModule != NULL) {
pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc is a new reference */
if (pFunc && PyCallable_Check(pFunc)) {
pArgs = PyTuple_New(argc - 3);
for (i = 0; i < argc - 3; ++i) {
pValue = PyLong_FromLong(atoi(argv[i + 3]));
if (!pValue) {
Py_DECREF(pArgs);
Py_DECREF(pModule);
fprintf(stderr, "Cannot convert argument\n");
return 1;
}
/* pValue reference stolen here: */
PyTuple_SetItem(pArgs, i, pValue);
}
pValue = PyObject_CallObject(pFunc, pArgs);
Py_DECREF(pArgs);
if (pValue != NULL) {
printf("Result of call: %ld\n", PyLong_AsLong(pValue));
Py_DECREF(pValue);
}
else {
Py_DECREF(pFunc);
Py_DECREF(pModule);
PyErr_Print();
fprintf(stderr, "Call failed\n");
return 1;
}
}
else {
if (PyErr_Occurred())
PyErr_Print();
fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
}
Py_XDECREF(pFunc);
Py_DECREF(pModule);
}
else {
PyErr_Print();
fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
return 1;
}
return 0;
}
void use_thread_a()
{
PythonThreadLocker locker;
int result = CallSomeFunction();
}
// 创建线程
int main(int argc, char *argv[])
{
Py_Initialize();
PyEval_InitThreads();
printf("%d", PyEval_ThreadsInitialized());
printf("a%d\n", PyGILState_Check());
Py_BEGIN_ALLOW_THREADS
printf("b%d\n", PyGILState_Check());
std::thread t1(use_thread_a);
std::thread t2(use_thread_a);
std::thread t3(use_thread_a);
std::thread t4(use_thread_a);
std::thread t5(use_thread_a);
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
printf("c%d\n", PyGILState_Check());
Py_END_ALLOW_THREADS
printf("d%d\n", PyGILState_Check());
CallSomeFunction();
Py_FinalizeEx();
return 0;
}
multiply.py 文件
def multiply(a,b):
print("Will compute", a, "times", b)
c = 0
for i in range(0, a):
c = c + b
return c
def hello2():
print('hello')
def tset_use_pd():
import pandas as pd
print(pd.DataFrame({'a':[1,2,3],'b':[4,5,6]}))
def test_raise_error():
raise ValueError('test raise valueerror')
def test_use_mulit_params(use_int, use_str: str, use_byte: bytes, use_list: list, use_tuple: tuple, use_dict: dict, use_complex):
print('use_int', use_int)
print('use_str', use_str)
print('use_byte', use_byte)
print('use_list', use_list)
print('use_tuple', use_tuple)
print('use_dict', use_dict)
print('use_complex', use_complex)
return (use_int, use_str, use_byte, use_list, use_tuple, use_dict, use_complex)
思考
作为一名前行的软件工程师,需要不断思考学习积累,绝不能急于求成,心浮气躁。随便百度搜索答案。虽然一天只做了一件事,但也是值得的。通过阅读官方文档,分析与实践同行,充分理解其含义,体会深刻。不然永远都不会明白程序为什么会 core dumps, wrong results, mysterious crashes。
加好友 & 工程下载
如果你和我有共同爱好,加好友一起学习吧!
更多推荐
C/C++ 多线程调用嵌入Python完整流程
发布评论