Повышение уровня Python, распространение обратных вызовов C++ на Python, вызывающих ошибку сегментации

У меня есть следующий прослушиватель на С++, который получает объект Python для распространения обратных вызовов.

class PyClient {
    private:
        std::vector<DipSubscription *> subs;

        subsFactory *sub;

        class GeneralDataListener: public SubscriptionListener {
            private:
                PyClient * client;

            public:
                GeneralDataListener(PyClient *c):client(c){
                    client->pyListener.attr("log_message")("Handler created");
                }

                void handleMessage(Subscription *sub, Data &message) {
                    // Lock the execution of this method
                    PyGILState_STATE state = PyGILState_Ensure();
                    client->pyListener.attr("log_message")("Data received for topic");
                    ...
                    // This method ends modifying the value of the Python object
                    topicEntity.attr("save_value")(valueKey, extractDipValue(valueKey.c_str(), message))
                    // Release the lock
                    PyGILState_Release(state);
                }

                void connected(Subscription *sub) {
                    client->pyListener.attr("connected")(sub->getTopicName());
                }

                void disconnected(Subscription *sub, char* reason) {
                    std::string s_reason(reason);
                    client->pyListener.attr("disconnected")(sub->getTopicName(), s_reason);
                }

                void handleException(Subscription *sub, Exception &ex) {
                    client->pyListener.attr("handle_exception")(sub->getTopicName())(ex.what());
                }
        };

        GeneralDataListener *handler;

    public:
        python::object pyListener;


        PyClient(python::object pyList): pyListener(pyList) {
            std::ostringstream iss;
            iss << "Listener" << getpid();
            sub = Sub::create(iss.str().c_str());
            createSubscriptions();
        }

        ~PyClient() {
            for (unsigned int i = 0; i < subs.size(); i++) {
                if (subs[i] == NULL) {
                    continue;
                }

                sub->destroySubscription(subs[i]);
            }
        }
};


BOOST_PYTHON_MODULE(pytest)
{
    // There is no need to expose more methods as will be used as callbacks
    Py_Initialize();
    PyEval_InitThreads();
    python::class_<PyClient>("PyClient",     python::init<python::object>())
        .def("pokeHandler", &PyClient::pokeHandler);
};

Затем у меня есть моя программа Python, которая выглядит так:

import sys
import time

import pytest


class Entity(object):
    def __init__(self, entity, mapping):
        self.entity = entity
        self.mapping = mapping
        self.values = {}
        for field in mapping:
            self.values[field] = ""

        self.updated = False

    def save_value(self, field, value):
        self.values[field] = value
        self.updated = True


class PyListener(object):
    def __init__(self):
        self.listeners = 0
        self.mapping = ["value"]

        self.path_entity = {}
        self.path_entity["path/to/node"] = Entity('Name', self.mapping)

    def connected(self, topic):
        print "%s topic connected" % topic

    def disconnected(self, topic, reason):
        print "%s topic disconnected, reason: %s" % (topic, reason)

    def handle_message(self, topic):
        print "Handling message from topic %s" % topic

    def handle_exception(self, topic, exception):
        print "Exception %s in topic %s" % (exception, topic)

    def log_message(self, message):
       print message

    def sample(self):
        for path, entity in self.path_entity.iteritems():
            if not entity.updated:
                return False

            sample = " ".join([entity.values[field] for field in dip_entity.mapping])
            print "%d %s %d %s" % (0, entity.entity, 4324, sample)
            entity.updated = False

        return True


if __name__ == "__main__":
    sys.settrace(trace)
    py_listener = PyListener()
    sub = pytest.PyClient(py_listener)

    while True:
        if py_listener.sample():
            break

Итак, наконец, моя проблема заключается в том, что когда я запускаю while True в программе Python, скрипт застревает, проверяя, обновлен ли объект, и случайным образом, когда слушатель C++ пытается вызвать обратный вызов, я получаю ошибку сегментации.

То же самое, если я просто попробую time.sleep в скрипте python и вызову время выборки по времени. Я знаю, что это будет решено, если я вызову образец из кода C++, но этот скрипт будет запущен другим модулем Python, который вызовет метод образца с определенной задержкой. Таким образом, ожидаемое функционирование будет для C++ для обновления значения объекты и скрипт Python, чтобы просто прочитать их.

Я отлаживаю ошибку с помощью gdb, но трассировка стека, которую я получаю, мало что объясняет:

#0  0x00007ffff7a83717 in PyFrame_New () from /lib64/libpython2.7.so.1.0
#1  0x00007ffff7af58dc in PyEval_EvalFrameEx () from /lib64/libpython2.7.so.1.0
#2  0x00007ffff7af718d in PyEval_EvalCodeEx () from /lib64/libpython2.7.so.1.0
#3  0x00007ffff7af7292 in PyEval_EvalCode () from /lib64/libpython2.7.so.1.0
#4  0x00007ffff7b106cf in run_mod () from /lib64/libpython2.7.so.1.0
#5  0x00007ffff7b1188e in PyRun_FileExFlags () from /lib64/libpython2.7.so.1.0
#6  0x00007ffff7b12b19 in PyRun_SimpleFileExFlags () from /lib64/libpython2.7.so.1.0
#7  0x00007ffff7b23b1f in Py_Main () from /lib64/libpython2.7.so.1.0
#8  0x00007ffff6d50af5 in __libc_start_main () from /lib64/libc.so.6
#9  0x0000000000400721 in _start ()

И если отлаживать с помощью sys.trace внутри Python, последняя строка перед ошибкой сегментации всегда находится в методе примера, но она может отличаться.

Я не уверен, как я могу решить эти проблемы со связью, поэтому любые советы в правильном направлении будут высоко оценены.

Изменить Измените ссылку PyDipClient на PyClient.

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

Если я удалю все обратные вызовы для слушателя Python и принудительно вызову методы из Python (например, вызов pokehandler), все будет работать отлично.


person KBorja    schedule 22.07.2015    source источник
comment
Слишком много недостающих частей для диагностики проблемы (например, PyDipClient, многопоточность и т. д.). mcve поможет другим диагностировать проблему.   -  person Tanner Sansbury    schedule 22.07.2015
comment
Я немного обновил вопрос, PyDipClient - это PyClient (забудьте об этом при переписывании).   -  person KBorja    schedule 23.07.2015
comment
Если вы можете предоставить MCVE (часто это сильно отличается от простого размещения существующего кода), то я могу предоставить дополнительную помощь, помимо предположения о том, что это проблема управления GIL. Я бы настоятельно рекомендовал всем, кто использует библиотеку, в которой внутренний поток обращается к пользовательскому коду, быть осторожным и хорошо ознакомиться с поведением потоков.   -  person Tanner Sansbury    schedule 23.07.2015


Ответы (1)


Наиболее вероятной причиной является то, что поток не удерживает Global Interpreter Lock (GIL). когда он вызывает код Python, что приводит к неопределенному поведению. Убедитесь, что все пути, по которым выполняются вызовы Python, такие как функции GeneralDataListener, получают GIL перед вызовом кода Python. Если создаются копии PyClient, то pyListener необходимо управлять таким образом, чтобы GIL мог сохраняться при его копировании и уничтожении.

Кроме того, рассмотрите правило трех для PyClient. Нужно ли конструктору копирования и оператору присваивания что-то делать с подпиской?


GIL — это мьютекс вокруг интерпретатора CPython. Этот мьютекс предотвращает выполнение параллельных операций над объектами Python. Таким образом, в любой момент времени максимум одному потоку, получившему GIL, разрешено выполнять операции с объектами Python. При наличии нескольких потоков вызов кода Python без удержания GIL приводит к неопределенному поведению.

Потоки C или C++ иногда называют чужими потоками в документации Python. Интерпретатор Python не имеет возможности управлять чужим потоком. Следовательно, чужие потоки отвечают за управление GIL, чтобы разрешить параллельное или параллельное выполнение с потоками Python.

В текущем коде:

  • GeneralDataListener::handle_message() управляет GIL безопасным способом без исключений. Например, если метод слушателя log_message() выдает исключение, стек раскручивается и не освобождает GIL, поскольку PyGILState_Release() не будет вызываться.

    void handleMessage(...)
    {
      PyGILState_STATE state = PyGILState_Ensure();
      client->pyListener.attr("log_message")(...);
      ...
    
      PyGILState_Release(state); // Not called if Python throws.
    }
    
  • GeneralDataListener::connected(), GeneralDataListener:: disconnected() и GeneralDataListener:: handleException() явно вызывают код Python, но явно не управляют GIL. Если вызывающая сторона не владеет GIL, вызывается неопределенное поведение, поскольку код Python выполняется без GIL.

    void connected(...)
    {
      // GIL not being explicitly managed.
      client->pyListener.attr("connected")(...);
    }
    
  • Неявно созданный конструктор копирования и оператор присваивания PyClient не управляют GIL, но могут косвенно вызывать код Python при копировании члена данных pyListener. Если делаются копии, вызывающая сторона должна удерживать GIL, когда объект PyClient::pyListener копируется и уничтожается. Если pyListener не управляется в свободном пространстве, то вызывающая сторона должна знать Python и получить GIL во время уничтожения всего объекта PyClient.

Чтобы решить их, подумайте:

Рекомендуем прочитать этот ответ, чтобы узнать больше об обратных вызовах в Python и тонкостях, таких как управление GIL во время создания и уничтожения копирования.

person Tanner Sansbury    schedule 23.07.2015
comment
Спасибо, я попробую, как только смогу, но это имеет смысл. - person KBorja; 24.07.2015
comment
Потрясающий! Благодарю вас! - person Aviad; 14.08.2017