C/C++ 多线程调用嵌入Python完整流程 您所在的位置:网站首页 c创建多线程 C/C++ 多线程调用嵌入Python完整流程

C/C++ 多线程调用嵌入Python完整流程

#C/C++ 多线程调用嵌入Python完整流程| 来源: 网络整理| 查看: 265

最近都很忙,忙着把公司的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 #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.org/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.org/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。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有