Встраивание Python в C/C++: первая часть - Многопоточное встраивание Python

ОГЛАВЛЕНИЕ

Многопоточное встраивание Python

Пора перейти к серьезному делу. Модулю Python и приложению C/C++ порой приходится работать одновременно, например, при моделировании. Допустим, встраиваемый модуль Python является частью моделирования в реальном времени и работает параллельно с остальной частью моделирования. Между тем он взаимодействует с остальной частью во время выполнения. Общепринятый метод - многопоточность. Есть несколько вариантов многопоточного встраивания. Здесь будут рассмотрены два из них.

При одном подходе создается отдельный поток в C, и вызывается модуль Python из функции потока. Это естественно и правильно, не считая того, что приходится защищать состояние интерпретатора Python. По сути, интерпретатор Python блокируется перед его использованием и освобождается после использования, так что Python может отслеживать его состояния для разных вызывающих потоков. Python предоставляет глобальную блокировку для этой цели. Ниже приведен полный исходный код "call_thread.c":

// call_thread.c – пример встраивания python
// (поток C, вызывающий функции python)
//
#include <Python.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#ifdef WIN32 // включения Windows
#include <Windows.h>
#include <process.h>
#define sleep(x) Sleep(1000*x)
HANDLE handle;
#else // включения POSIX
#include <pthread.h>
pthread_t mythread;
#endif

void ThreadProc(void*);

#define NUM_ARGUMENTS 5
typedef struct
{
int argc;
char *argv[NUM_ARGUMENTS];
} CMD_LINE_STRUCT;

int main(int argc, char *argv[])
{
int i;
CMD_LINE_STRUCT cmd;
pthread_t mythread;

cmd.argc = argc;
for( i = 0; i < NUM_ARGUMENTS; i++ )
{
cmd.argv[i] = argv[i];
}

if (argc < 3)
{
fprintf(stderr,
"Usage: call python_filename function_name [args]\n");
return 1;
}

// создать поток
#ifdef WIN32
// код Windows
handle = (HANDLE) _beginthread( ThreadProc,0,&cmd);
#else
// код POSIX
pthread_create( &mythread, NULL,
ThreadProc, (void*)&cmd );
#endif

// произвольный тестовый код
for(i = 0; i < 10; i++)
{
printf("Printed from the main thread.\n");
sleep(1);
}

printf("Main Thread waiting for My Thread to complete...\n");

// присоединить и ждать завершения созданного потока...
#ifdef WIN32
// код Windows
WaitForSingleObject(handle,INFINITE);
#else
// код POSIX
pthread_join(mythread, NULL);
#endif

printf("Main thread finished gracefully.\n");

return 0;
}

void ThreadProc( void *data )
{
int i;
PyObject *pName, *pModule, *pDict,
*pFunc, *pInstance, *pArgs, *pValue;
PyThreadState *mainThreadState, *myThreadState, *tempState;
PyInterpreterState *mainInterpreterState;

CMD_LINE_STRUCT* arg = (CMD_LINE_STRUCT*)data;

// произвольный тестовый код
for(i = 0; i < 15; i++)
{
printf("...Printed from my thread.\n");
sleep(1);
}

// инициализировать интерпретатор python
Py_Initialize();

// инициализировать поддержку потоков
PyEval_InitThreads();

// сохранить указатель на главный объект PyThreadState
mainThreadState = PyThreadState_Get();

// получить ссылку на PyInterpreterState
mainInterpreterState = mainThreadState->interp;

// создать объект состояния потока для этого потока
myThreadState = PyThreadState_New(mainInterpreterState);

// снять глобальную блокировку
PyEval_ReleaseLock();

// получить глобальную блокировку
PyEval_AcquireLock();

// загрузить мое состояние потока
tempState = PyThreadState_Swap(myThreadState);

// выполнить некоторый код python (вызвать функции python)
pName = PyString_FromString(arg->argv[1]);
pModule = PyImport_Import(pName);

// pDict и pFunc – заимствованные ссылки
pDict = PyModule_GetDict(pModule);
pFunc = PyDict_GetItemString(pDict, arg->argv[2]);

if (PyCallable_Check(pFunc))
{
pValue = PyObject_CallObject(pFunc, NULL);
}
else {
PyErr_Print();
}

// возврат ресурсов системе
Py_DECREF(pModule);
Py_DECREF(pName);

// выгрузить текущий поток
PyThreadState_Swap(tempState);

// снять глобальную блокировку
PyEval_ReleaseLock();

// очистить состояние потока
PyThreadState_Clear(myThreadState);
PyThreadState_Delete(myThreadState);

Py_Finalize();
printf("My thread is finishing...\n");

// выход из потока
#ifdef WIN32
// код Windows
_endthread();
#else
// код POSIX
pthread_exit(NULL);
#endif
}

Функция потока требует немного пояснений. PyEval_InitThreads() инициализирует поддержку потоков Python. PyThreadState_Swap(myThreadState) загружает состояние для текущего потока, а PyThreadState_Swap(tempState) выгружает его. Интерпретатор Python сохраняет то, что происходит между двумя вызовами, как данные о состоянии, связанном с этим потоком. Вообще, Python сохраняет данные для каждого потока, использующего интерпретатор, так что состояния потоков взаимоисключающие. Но необходимо создать и сохранять состояние для каждого потока C. Сначала не был вызван PyEvel_AcquireLock(), потому что PyEval_InitThreads() делает это по умолчанию. В других случаях надо использовать PyEvel_AcquireLock() и PyEvel_ReleaseLock() попарно.

Выполните "call_thread py_thread pythonFunc" и получите показанный ниже вывод. Файл "py_thread.py" определяет функцию по имени pythonFunc(), в которой похожий произвольный тестовый блок печатает "распечатано из pythonFunc..." на экране 15 раз.

Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
...Printed from my thread.
Printed from the main thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Main Thread waiting for My Thread to complete...
...Printed from my thread.
...Printed from my thread.
...Printed from my thread.
...Printed from my thread.
...Printed from my thread.
My thread is finishing...
Main thread finished gracefully.

Разумеется, реализация усложняется, потому что писать многопоточный код на C/C++ непросто. Несмотря на то, что код переносимый, он содержит многочисленные «заплаты», требующие досконального знания интерфейсов вызова системы для конкретных платформ. К счастью, Python сделал большую часть этого, что приводит к рассмотрению второго решения, а именно, позволить Python управлять многопоточностью. На этот раз код Python усовершенствован путем добавления модели поточной обработки:

'''Показать использование поточной обработки python'''
import time
import threading

class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
for i in range(15):
print 'printed from MyThread...'
time.sleep(1)

def createThread():
print 'Create and run MyThread'
background = MyThread()
background.start()
print 'Main thread continues to run in foreground.'
for i in range(10):
print 'printed from Main thread.'
time.sleep(1)
print 'Main thread joins MyThread and waits until it is done...'
background.join() # ждать завершения фоновой задачи
print 'The program completed gracefully.'

Код C больше не управляет поточной обработкой. Он только вызывает createThread(). Попробуйте использовать предыдущий "call_function.c". Запустите "call_function py_thread createThread", чтобы увидеть, как выглядит вывод. Второе решение опрятней и проще. Более того, модель поточной обработки Python переносима. В то время как код поточной обработки C для Unix и Windows различается, в Python он остается неизменным.

Функция Python createThread() не нужна, если код C вызывает методы start() и joint() класса потока. Соответствующие изменения перечислены ниже (из исходного файла C "call_thread_2.c"):

// создать экземпляр
pInstance = PyObject_CallObject(pClass, NULL);

PyObject_CallMethod(pInstance, "start", NULL);

i = 0;
while(i<10)
{
printf("Printed from C thread...\n");

// !!!важно!!! поток C не освободит процессор для
// потока Python без следующего вызова.
PyObject_CallMethod(pInstance, "join", "(f)", 0.001);
Sleep(1000);
i++;
}

printf(
"C thread join and wait for Python thread to complete...\n");
PyObject_CallMethod(pInstance, "join", NULL);

printf("Program completed gracefully.\n");

После создания экземпляра класса вызывается его метод start() для создания нового потока, и выполняется его метод run(). Без частых коротких присоединений к созданному потоку созданный поток может выполниться только в начале, и главный поток не освободит для него процессор, пока не завершится. Можно проверить это, закомментировав вызов присоединения в цикле while. Поведение отличается от предыдущего случая, где вызывался start() из модуля Python. Видимо, это свойство многопоточности, не документированное в справочнике по библиотеке Python.

Интересные особенности

Намеренно обращалось внимание на написание обобщенного, переносимого кода C для встраивания Python. Путем инкапсуляции низкоуровневых системных вызовов Python поддерживает переносимость между платформами и упрощает написание переносимого кода. Большинство модулей Python легко переносятся между средами Unix и Windows. Надо учитывать переносимость при написании обертывающего кода C/C++ для модулей Python. Не всегда легко самостоятельно писать переносимый код C. Python сделал большую часть трудной работы, как в вышеуказанном случае. Ищите более простые и аккуратные решения. Так или иначе, на этом все. Инструкция по написанию переносимого кода Python выходит за рамки данной статьи. Это был бы хороший заголовок для новой статьи.

Несмотря на то, что встраивание является хорошим вариантом использования модулей Python в приложениях C/C++, есть другие альтернативные подходы. В Windows некоторые инструменты (вроде "py2exe") преобразуют модули Python непосредственно в исполнимые модули Windows. Затем порождается процесс, чтобы запустить исполнимый модуль из приложения C/C++. Недостаток в том, что нельзя вызвать модуль напрямую. Приложению приходится взаимодействовать с модулем через определенные типы взаимодействия между процессами (IPC). При этом рассматриваемый модуль Python должен быть "готов к IPC", то есть реализовывать интерфейс IPC для обработки входящих и исходящих данных. Вторая часть этой статьи рассмотрит связанные с встраиванием приемы IPC.

Весь предоставленный в статье исходный код является простым кодом C ради демонстрации. На практике рекомендуется помещать встраивающий Python код в классы-обертки C++. При этом разработчикам высокоуровневых приложений не придется иметь дело с деталями встраивания.

Заключение

В этой части было рассмотрено встраивание Python от азов, таких как вызов функций, классов и методов, до более трудных тем типа многопоточного встраивания. Python/C API предоставляет логичный интерфейс вызова, облегчающий объединение C/C++ и модулей Python.

Обсуждение многопоточного встраивания поставило вопрос: как приложение C/C++ взаимодействует со встроенным модулем Python? Вторая часть данной статьи рассматривает этот вопрос с позиции IPC.