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參考手冊》

 

  如有錯誤缺漏,務必要告訴我噢!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM