Python 3.X 调用多线程C模块,并在C模块中回调python函数的示例

  由于最近在做一个C++面向Python的API封装项目,因此需要用到C扩展Python的相关知识。在此进行简要的总结。

  此篇示例分为三部分。第一部分展示了如何用C在Windows中进行多线程编程;第二部分将第一部分的示例进行扩展,展示了如何在python中调用多线程的C模块;第三部分扩展了第二部分,增加了在C模块的线程中回调python的演示。

  本文所用的环境为:64位Win7 + python 3.4 x86 + vs2010

一、windows下的C语言多线程程序

  windows下多线程编程比较简单,第一步是包含<windows.h>的头文件,第二步是定义线程函数,第三步在主线程中创建线程并传入线程函数。最后注意要释放线程句柄,避免句柄泄露(不等同于线程泄露)。

  在vs2010中新建一个win32控制台应用程序,附加选项中勾选空项目,点完成。新建一个test.cpp的源文件代码如下:

 1 #include <stdio.h>
 2 #include <windows.h>
 3 #include <iostream>
 4 using namespace std;
 5
 6 bool flag;
 7
 8 DWORD WINAPI setFlag(LPVOID lpParamter) {
 9     cout<<"[Thread1]: start\n";
10     Sleep(10000);
11     cout<<"[Thread1]:now i set flag to true, exit!\n";
12     flag = true;
13     return 0;
14 }
15
16 DWORD WINAPI doSomething(LPVOID lpParamter) {
17     cout<<"[Thread2]:start\n";
18     while(flag==false) {
19         Sleep(1000);
20         cout<<"[Thread2]:flag is false, wait...\n";
21     }
22     cout<<"[Thread2]:oh, flag is true now! exit!\n";
23     flag = false;
24     return 0;
25 }
26
27 int main(){
28     cout<<"[MainThread]:start\n";
29     flag = false;
30
31     HANDLE hTread1 = CreateThread(NULL, 0, setFlag, NULL, 0, NULL);        // 创建线程
32     CloseHandle(hTread1);        // 通知Windows该句柄已经不需要再使用
33     HANDLE hTread2 = CreateThread(NULL, 0, doSomething, NULL, 0, NULL);
34     CloseHandle(hTread2);
35     cout<<"[MainThread]:exit\n";
36     getchar();
37     return 0;
38 }

  最终的结果如下(机器不同可能有所出入):

  下面是函数的原型

HANDLE CreateThread(
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    SIZE_T dwStackSize,
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID lpParameter,
    DWORD dwCreationFlags,
    LPDWORD lpThreadId
)

BOOL CloseHandle(
    HANDLE hObject
);

  CreateThread参数解释:

  lpThreadAttributes:指向SECURITY_ATTRIBUTES型态的结构的指针。在Windows 98中忽略该参数。在Windows NT中,NULL使用默认安全性,不可以被子线程继承,否则需要定义一个结构体将它的bInheritHandle成员初始化为TRUE
  dwStackSize,设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小。任何情况下,Windows根据需要动态延长堆栈的大小。
  lpStartAddress,指向线程函数的指针,形式:@函数名,函数名称没有限制,但是必须以下列形式声明:

DWORD WINAPI 函数名 (LPVOID lpParam)  //格式不正确将无法调用成功。

  lpParameter:向线程函数传递的参数,是一个指向结构的指针,不需传递参数时,为NULL。
  dwCreationFlags :线程标志,可取值如下
    (1)CREATE_SUSPENDED(0x00000004):创建一个挂起的线程,
    (2)0:表示创建后立即激活。
    (3)STACK_SIZE_PARAM_IS_A_RESERVATION(0x00010000):dwStackSize参数指定初始的保留堆栈 的大小,否则,dwStackSize指定提交的大小。该标记值在Windows 2000/NT and Windows Me/98/95上不支持。
  lpThreadId:保存新线程的id。

  返回值:函数成功,返回线程句柄;函数失败返回false。若不想返回线程ID,设置值为NULL。

  CloseHandle参数解释:

  hObject :代表一个已打开对象handle。
  返回值:TRUE:执行成功;FALSE:执行失败,可以调用GetLastError()获知失败原因。

  AI:

  1,线程和线程句柄(Handle)不是一个东西,线程是在cpu上运行的.....(说不清楚了),线程句柄是一个内核对象。我们可以通过句柄来操作线程,但是线程的生命周期和线程句柄的生命周期不一样的。线程的生命周期就是线程函数从开始执行到return,线程句柄的生命周期是从CreateThread返回到你CloseHandle()。
  2,线程句柄是一种内核对象,系统维护着每一个内核对象,当每个内核对象引用记数为0时,系统就从内存中释放该对象,CloseHandle就是将该线程对象的引用记数减1。所有的内核对象(包括线程Handle)都是系统资源,用了要还的,也就是说用完后一定要closehandle关闭之,如果不这么做,你系统的句柄资源很快就用光了。
  只是关闭了一个线程句柄对象,表示我不再使用该句柄,即不对这个句柄对应的线程做任何干预了。并没有结束线程。

二、python中调用C模块的示例

  python是个有趣的玩意,一开始只是想学来做个项目,结果越学越上瘾,就好像从贫瘠的德拉诺瞬间穿越到物质丰富的艾泽拉斯,打开了一个新世界的大门....当然这是题外话...

  python号称粘性语言,它可以很方便的调用C的模块,从而做到C能做到的一切....

  下面在第一部分的基础上,展示了如何使用python/C api,让python能够调用c语言写出来的模块。

  Python调用C函数我把它分为四小步:

  1.为VS2010中添加python支持,包括在项目的引用目录中添加python34\include,库目录中添加python34\libs,链接库附加库目录中加入python34\dlls,然后在代码中引入Python.h头文件

  2.通过python自带的C API,在源码中定义对python的导出函数,然后定义模块的基本信息。

  3.编译为动态链接库(windows下为dll,linux下为.so),并更名为.pyd。

  4.直接在python中import然后使用。

  下面我们开始吧。

  重新在vs2010中新建一个win32项目,我命名为mytest,这次我们要选择DLL的应用程序类型。

  

  完成之后在源码中新建mytest.cpp,把示例一中的代码全都复制进去,然后在文件开头引入Python/C API

#include <Python.h>

  为了方便看效果,我们注释掉main()函数中的"getchar();"像下面这样。。。

int main(){
    cout<<"[MainThread]:start\n";
    flag = false;

    HANDLE hTread1 = CreateThread(NULL, 0, setFlag, NULL, 0, NULL);        // 创建线程
    CloseHandle(hTread1);        // 通知Windows该句柄已经不需要再使用
    HANDLE hTread2 = CreateThread(NULL, 0, doSomething, NULL, 0, NULL);
    CloseHandle(hTread2);
    cout<<"[MainThread]:exit\n";
    //getchar();
    return 0;
}

  然后我们定义一个导出函数(在函数中调用原汁原味的main()...),然后返回一个PyObject*。

PyObject* wrap_main(PyObject* self, PyObject* args)
{
    main();
    return Py_BuildValue("i", 0);
}

  我们知道,python中所有东东都是对象,映射到C里面其实就相当于一个PyObject*。其中的Py_BuildValue("i", 0)用于生成一个PyObject*,其值相当于整型的0。

  Py_BuildValue的详情可以看这里:https://docs.python.org/3/c-api/arg.html?highlight=py_buildvalue#c.Py_BuildValue

  接着我们要定义一个函数导出列表,说明我们要导出的函数,像下面这样...

// 定义导出函数列表
static PyMethodDef module_methods[] = {
    {"main", wrap_main, METH_NOARGS, "start thread1 and thread2"},    // METH_NOARGS表示不接收任何参数
    {NULL, NULL, 0, NULL}
};

  PyMethodDef第一个参数指定要导出的函数名称(可以直接在python中用module.xxx()调用),第二个参数指定具体的实现了python/C API的c函数,第三个参数指定函数的参数类型,第四个参数为函数的说明。

  详细定义看这里:https://docs.python.org/3/c-api/structures.html#c.PyMethodDef(官网)

  然后定义模块信息,并提供一个模块初始化函数:

// 模块基本信息的定义
static PyModuleDef moduledef = {
    PyModuleDef_HEAD_INIT,
    "mytest",            // 模块名
    "test thread",        // 模块说明
    -1,
    module_methods        // 导出函数列表
};

// 模块初始化函数
PyMODINIT_FUNC PyInit_mytest(void) {
    PyObject* m;
    m= PyModule_Create(&moduledef);
    return m;
}

  注意:初始化函数名一定要是PyInit_模块名(void)的形式,在示例中模块名为mytest,所以模块初始化函数为,PyInit_mytest(void)

  源码编辑大功告成。

  不要忘记我们一开始说过的环境配置,右键点击项目-> 属性 。在引用目录中添加python34\include,库目录中添加python34\libs,链接库->附加库目录中加入python34\dlls,点击确定。

  生成方式记得要选择Release(Debug模式需要python34_b.dll,然而我们的二进制python包里面没有),然后右键点击项目 -> 生成  。

  我们可以在该项目的release目录下找到生成好的mytest.dll,改为pyd后缀:mytest.pyd。

  然后就打开cmd控制台,cd进入项目的release目录,开启python(ipython)进行测试:

import mytest
mytest.main()

  因为python在执行完c模块中的主线程之后会直接返回,导致一开始的输出乱象——这是正常现象。

  如果我们没有注释掉main()中的getchar();  那么我们可以等待线程执行完以后再敲下回车,这样就不会出现输出乱象。

  恭喜您已经完成了此次示例的90%!!

三、C中线程回调python函数的演示

  当你完成了前面两步,这一步其实非常简单!

  我们的目标是在Thread2,也就是doSomething函数返回之前,调用python中定义的函数。所以,我们要对doSomething进行改造,让其接收一个Python方法,也就是PyObject*对象,并进行回调。

DWORD WINAPI doSomething(LPVOID callback) {
    cout<<"[Thread2]:start\n";
    while(flag==false) {
        Sleep(1000);
        cout<<"[Thread2]:flag is false, wait...\n";
    }
    cout<<"[Thread2]:oh, flag is true now! Let‘s exit and call the Python!!!!\n";
    PyGILState_STATE state = PyGILState_Ensure();        // 获取GIL控制权限
    PyEval_CallObject((PyObject*)callback, NULL);        // 回调python函数
    PyGILState_Release(state);                            // 释放GIL控制权
    flag = false;
    return 0;
}

  需要说明的是LPVOID本质是一个void*,因此我们可以偷懒,不必更改参数类型,在PyEval_CallObject中强制转换一下类型即可。

PyObject* PyEval_CallObject(PyObject* pfunc, PyObject* pargs)

  此函数有两个参数,而且都是Python对象指针,其中pfunc是要调用的Python 函数,一般说来可以使用PyObject_GetAttrString()获得,pargs是函数的参数列表,通常是使用Py_BuildValue()来构建。

  我们的回调函数中并不准备接收参数,所以pargs直接为NULL。

  微调一下main函数,使其接收一个PyObject*类型的参数

int main(PyObject* callback){    // 接收一个PyObject*参数
    cout<<"[MainThread]:start\n";
    flag = false;

    HANDLE hTread1 = CreateThread(NULL, 0, setFlag, NULL, 0, NULL);        // 创建线程
    CloseHandle(hTread1);        // 通知Windows该句柄已经不需要再使用
    HANDLE hTread2 = CreateThread(NULL, 0, doSomething, callback, 0, NULL);  // 传入callback
    CloseHandle(hTread2);
    cout<<"[MainThread]:exit\n";
    //getchar();
    return 0;
}

  然后修改一下wrap_main函数,转换获取到的参数对象:

PyObject* wrap_main(PyObject* self, PyObject* args)
{
    PyObject* callback;    // 用于获取回调函数的PyObjectzhizhe
    PyArg_Parse(args, "(O)", &callback);    // 类型转换
    Py_XINCREF(callback);    // 增加计数
    main(callback);    //调用启用函数
    return Py_BuildValue("i", 0);
}

  函数:int PyArg_Parse(PyObject* args, char* format, ...)

  含义:把Python数据类型解析为C的类型,这样C程序中才可以使用Python里面的数据。

  宏定义:Py_INCREF(obj)/Py_DECREF()
     说明:增加或减少对象obj的引用计数。Py_INCREF()和Py_DECREF()两个函数也有一个先检查对象是否为空的版本,分别为Py_XINCREF()和Py_XDECREF()。编译扩展的程序员必须要注意,代码有可能会被运行在一个多线程的Python环境中。这些线程使用了两个C宏Py_BEGIN_ALLOW_THREADS和Py_END_ALLOW_THREADS,通过将代码和线程隔离,保证了运行和非运行时的安全性,由这些宏包裹的代码将会允许其他线程的运行。

  详细信息可见官方文档:https://docs.python.org/3/extending/extending.html#reference-counts

  最后一步是修改函数导出中的参数定义,允许函数接收参数:

// 定义导出函数列表
static PyMethodDef module_methods[] = {
    {"main", wrap_main, METH_VARARGS, "start thread1 and thread2"},    // META_VARARGS表示允许接收可变数量的参数
    {NULL, NULL, 0, NULL}
};

  修改好之后,重新生成DLL,并更改后缀名为pyd。完成!~~~

  最后又到了进行测试的时间!测试代码:

import mytest

def callback():
    print("Oh, I am comeback!!")

mytest.main(callback)

  当当~~结果又正如你所料!

  恭喜你!你已经成功穿越了python与C之间的恶魔之门!^v^

推荐扩展阅读:

教你如何导出C++类到Python,同样也是使用python/c api:《C++导出类到Python》[半根稻草]

Python最权威的文档,官方文档

Python最权威的手册官方《Python/C API参考手册》

  如有错误缺漏,务必要告诉我噢!

时间: 2024-09-30 00:50:11

Python 3.X 调用多线程C模块,并在C模块中回调python函数的示例的相关文章

Python学习教程(Python学习路线):教你如何在交互式环境中执行Python程序

Python学习教程(Python学习路线):教你如何在交互式环境中执行Python程序 相信接触过Python的伙伴们都知道运行Python脚本程序的方式有多种,目前主要的方式有:交互式环境运行.命令行窗口运行.开发工具上运行等,其中在不同的操作平台上还互不相同.今天,小编讲些Python基础的内容,以Windows下交互式环境为依托,演示Python程序的运行. 一般来说,顺利安装Python之后,有两种方式可以进入Python交互性环境.一种是在Python自带的IDLE中直接打开交互式窗

C#中调用C++的dll的参数为指针类型的导出函数(包括二级指针的情况)

严格来说这篇文章算不上C++范围的,不过还是挂了点边,还是在自己的blog中记录一下吧. C++中使用指针是家常便饭了,也非常的好用,这也是我之所以喜欢C++的原因之一.但是在C#中就强调托管的概念了,指针就不用想了.本来如果就在C#的世界里面写代码,也还算舒服,但是万事万物总有联系,这不,现在公司的另外一个用C#作的项目就碰到问题了,要调用之前用C++写的一个DLL中的一些函数,很多函数的参数都是指针类型的,这下可麻烦咯,公司里做C#的都是刚起步,C++又只有我最熟悉,这项技术研究工作又光荣的

python 学习笔记day10-python多线程,forking,xinetd服务

xinetd服务器 配置xinetd服务 什么是xinetd xinetd可以统一管理很多服务进程,它能够: - 绑定.侦听和接受来对自服务器每个端口的请求 - 有客户访问时,调用相应的服务器程序相应 - 节约了系统内存资源 - 同时响应多个客户端的连接请求 Windows系统没有该功能 多数UNIX系统使用的是inetd实现相同的功能 配置文件解析 选项名称 说明 flags 如果只指定NAMEINARGS,那么它就使参数和inetd一样的传递 type 如果服务不在/etc/services

一行 Python 实现并行化 -- 日常多线程操作的新思路

转自: http://www.zhangzhibo.net/2014/02/01/parallelism-in-one-line/          http://chriskiehl.com/article/parallelism-in-one-line/  Python 在程序并行化方面多少有些声名狼藉.撇开技术上的问题,例如线程的实现和 GIL1,我觉得错误的教学指导才是主要问题.常见的经典 Python 多线程.多进程教程多显得偏"重".而且往往隔靴搔痒,没有深入探讨日常工作中

python 并发执行之多线程

正常情况下,我们在启动一个程序的时候.这个程序会先启动一个进程,启动之后这个进程会拉起来一个线程.这个线程再去处理事务.也就是说真正干活的是线程,进程这玩意只负责向系统要内存,要资源但是进程自己是不干活的.默认情况下只有一个进程只会拉起来一个线程. 多线程顾名思义,就是同样在一个进程的情况同时拉起来多个线程.上面说了,真正干活的是线程.进程与线程的关系就像是工厂和工人的关系.那么现在工厂还是一个,但是干活的工人多了.那么效率自然就提高了.因为只有一个进程,所以多线程在提高效率的同时,并没有向系统

Python:线程、进程与协程(4)——multiprocessing模块(1)

multiprocessing模块是Python提供的用于多进程开发的包,multiprocessing包提供本地和远程两种并发,通过使用子进程而非线程有效地回避了全局解释器锁. (一)创建进程Process 类 创建进程的类,其源码在multiprocessing包的process.py里,有兴趣的可以对照着源码边理解边学习.它的用法同threading.Thread差不多,从它的类定义上就可以看的出来,如下: class Process(object):     '''     Proces

python 3.x 学习笔记16 (队列queue 以及 multiprocessing模块)

1.队列(queue) 用法: import queue q = queue.Queue() #先进先出模式 q.put(1) #存放数据在q里 作用: 1)解耦    2)提高效率 class queue.Queue(maxsize=0)                        #先入先出class queue.LifoQueue(maxsize=0)                  #后进先出 class queue.PriorityQueue(maxsize=0)         

python-多线程:调用thread模块中的start_new_thread()函数来产生新线程

Python 多线程 多线程类似于同时执行多个不同程序,多线程运行有如下优点: 使用线程可以把占据长时间的程序中的任务放到后台去处理. 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度 程序的运行速度可能加快 在一些等待的任务实现上如用户输入.文件读写和网络收发数据等,线程就比较有用了.在这种情况下我们可以释放一些珍贵的资源如内存占用等等. 线程在执行过程中与进程还是有区别的.每个独立的线程有一个程序运行的入口.顺序执行序列和程序的出口.

python并发编程之多线程(实践篇)

一.threading模块介绍 官网链接:https://docs.python.org/3/library/threading.html?highlight=threading# 1.开启线程的两种方式 #直接调用 import threading import time def run(n): print('task',n) time.sleep(2) t1 = threading.Thread(target=run,args=('t1',)) t1.start() #继承式调用 mport