2015-07-22 55 views
1

我在C++中有以下偵聽器,它接收一個Python對象來傳播回調。Boost Python,將C++回調傳播到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 

所以,最後,我的問題似乎是,當我開始運行,而真正的Python程序腳本如果實體更新,則會停滯不前,而隨機地,當C++偵聽器嘗試調用回調時,會出現分段錯誤。

同樣,如果我只是在python腳本中嘗試time.sleep並且每次調用一次樣本。我知道如果我從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() 

如果裏面的Python sys.trace調試的最後一行之前分割故障總是在示例方法中,但可能會有所不同。

我不知道我該如何解決這個溝通問題,所以任何正確的方向建議都將不勝感激。

編輯 將PyDipClient引用修改爲PyClient。

發生了什麼是我從Python主要方法啓動程序,如果然後C++偵聽器試圖回調它與分段錯誤錯誤崩潰的Python偵聽器,我相信創建的唯一線程是當我創建一個訂閱,但這是來自庫中的代碼,我不知道如何正確工作。

如果我刪除所有對Python偵聽器的回調,並強制Python的方法(如調用pokehandler),那麼所有的工作都是完美的。

+0

有太多缺失的部分來診斷問題(例如'PyDipClient',線程等)。 [mcve](http://stackoverflow.com/help/mcve)在幫助其他人幫助診斷問題方面會有很大的幫助。 –

+0

我更新了一些問題,PyDipClient是PyClient(重寫時忘記它)。 – KBorja

+0

如果你可以提供一個MCVE(這通常與僅僅發佈一個已有的代碼非常不同),那麼除了推測它是一個GIL管理問題之外,我可以提供進一步的幫助。我強烈鼓勵任何人使用庫,其中內部線程調用回用戶代碼時要謹慎,並且非常熟悉線程行爲。 –

回答

1

最有可能的罪魁禍首是Global Interpreter Lock(GIL)在調用Python代碼時未被線程佔用,導致未定義的行爲。驗證所有Python調用的路徑,例如GeneralDataListener的函數,在調用Python代碼之前獲取GIL。如果正在製作PyClient的副本,那麼需要對pyListener進行管理,以允許在複製和銷燬GIL時保持該狀態。

此外,考慮rule of threePyClient。複製構造函數和賦值運算符是否需要對訂閱進行任何操作?


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,但複製pyListener數據成員時,可能會間接調用Python代碼。如果正在製作副本,則在複製和銷燬PyClient::pyListener對象時,調用者需要保留GIL。如果pyListener未在可用空間上管理,則調用者必須是Python感知的,並在銷燬整個PyClient對象期間獲取GIL。

要解決這些問題,考慮:

  • 使用Resource Acquisition Is Initialization(RAII)後衛類來幫助一個異常安全的方式管理GIL。例如,使用以下gil_lock類,創建gil_lock對象時,調用線程將獲取GIL。當gil_lock目的是破壞,它釋放GIL

    /// @brief RAII class used to lock and unlock the GIL. 
    class gil_lock 
    { 
    public: 
        gil_lock() { state_ = PyGILState_Ensure(); } 
        ~gil_lock() { PyGILState_Release(state_); } 
    private: 
        PyGILState_STATE state_; 
    }; 
    
    ... 
    
    void handleMessage(...) 
    { 
        gil_lock lock; 
        client->pyListener.attr("log_message")(...); 
        ... 
    } 
    
  • 在可從一個外來線程內調用Python代碼的任何代碼路徑明確地管理的GIL。

    void connected(...) 
    { 
        gil_lock lock; 
        client->pyListener.attr("connected")(...); 
    } 
    
  • 製作PyClient不可複製或明確創建拷貝構造函數和賦值操作符。如果正在製作副本,則更改pyListener以保留GIL時允許顯式銷燬的類型。一種解決方案是使用boost::shared_ptr<python::object>來管理在構建期間提供給PyClientpython::object的副本,並且具有GIL認知的定製刪除器。或者,可以使用類似boost::optional的東西。

    class PyClient 
    { 
    public: 
    
        PyClient(const boost::python::object& object) 
        : pyListener(
         new boost::python::object(object), // GIL locked, so copy. 
         [](boost::python::object* object) // Delete needs GIL. 
         { 
          gil_lock lock; 
          delete object; 
         } 
        ) 
        { 
        ... 
        } 
    
    private: 
        boost::shared_ptr<boost::python::object> pyListener;; 
    }; 
    

    注意,通過在自由空間管理boost::python::object,一個可以自由複製shared_ptr沒有持有GIL。另一方面,如果有人使用類似boost::optional的東西來管理Python對象,那麼在複製構建,分配和銷燬過程中需要持有GIL。

考慮閱讀this答案複製構造和析構過程中的回調Python和微妙的細節,比如GIL管理的更多細節。

+0

謝謝,我會盡快給它一個鏡頭,但它是有道理的。 – KBorja

+0

太棒了!謝謝! – Aviad