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

最近都很忙,忙着把公司的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

加好友 & 工程下载

如果你和我有共同爱好,加好友一起学习吧!
C/C++ 多线程调用嵌入Python完整流程_第1张图片

你可能感兴趣的:(C++,Embedding,Python,thread)