Python C 扩展嵌套字典分段错误

Python C Extension Nested Dictionary Segmentation Fault

我正在尝试在 C 中创建一个 Python (2.7.12) 扩展,它执行以下操作:

我创建了这个扩展的简化版本,它向字典中添加了一个条目,然后不断地用新值修改它。下面是 C 文件,其中包含关于它正在做什么的评论以及我对如何处理引用计数的理解。

#include <Python.h>
#include <pthread.h>

static PyObject *module;
static PyObject *pyitem_error;
static PyObject *item;
static PyObject *item_handle;
static pthread_t thread;

void *stuff(void *param)
{
    int garbage = 0;
    PyObject *size;
    PyObject *value;

    while(1)
    {
        // Build a dictionary called size containg two integer objects
        // Py_BuildValue will pass ownership of its reference to size to this thread
        size = NULL;
        size = Py_BuildValue("{s:i,s:i}", "l", garbage, "w", garbage);
        if(size == NULL)
        {
            goto error;
        }

        // Build a dictionary containing an integer object and the size dictionary
        // Py_BuildValue will create and own a reference to the size dictionary but not steal it
        // Py_BuildValue will pass ownership of its reference to value to this thread
        value = NULL;            
        value = Py_BuildValue("{s:i,s:O}", "h", garbage, "base", size);
        if(value == NULL)
        {
            goto error;
        }

        // Add the new data to the dictionary
        // PyDict_SetItemString will borrow a reference to value         
        PyDict_SetItemString(item, "dim", value);

        error:
        Py_XDECREF(size);
        Py_XDECREF(value);
        garbage++;              
    }

    return NULL;
}

// There will be methods for this module in the future
static PyMethodDef pyitem_methods[] =
{
    {NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC initpyitem(void)
{
    // Create a module object
    // Own a reference to it since Py_InitModule returns a borrowed reference
    module = Py_InitModule("pyitem", pyitem_methods);
    Py_INCREF(module);

    // Create an exception object for future use
    // Own a second reference to it since PyModule_AddObject will steal a reference
    pyitem_error = PyErr_NewException("pyitem.error", NULL, NULL);
    Py_INCREF(pyitem_error);
    PyModule_AddObject(module, "error", pyitem_error);

    // Create a dictionary object and a proxy object that makes it read only
    // Own a second reference to the proxy object since PyModule_AddObject will steal a reference
    item = PyDict_New();
    item_handle = PyDictProxy_New(item);
    Py_INCREF(item_handle);
    PyModule_AddObject(module, "item", item_handle);

    // Start the background thread that modifies the dictionary
    pthread_create(&thread, NULL, stuff, NULL);
}

下面是一个使用此扩展的 Python 程序。它所做的只是打印出字典中的内容。

import pyitem

while True:
    print pyitem.item
    print

此扩展程序似乎可以工作一段时间,然后因分段错误而崩溃。对核心转储的检查揭示了以下内容:

Core was generated by `python pyitem_test.py'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  PyObject_Malloc (nbytes=nbytes@entry=42) at Objects/obmalloc.c:831
831             if ((pool->freeblock = *(block **)bp) != NULL) {
[Current thread is 1 (Thread 0x7f144a824700 (LWP 3931))]

这个核心转储让我相信这个问题可能与我对对象引用计数的处理有关。我相信这可能是一个原因,因为其他人使用相同的核心转储所带来的问题通过正确处理引用计数解决了这个问题。但是,我没有发现我对对象引用计数的处理有任何问题。

我想到的另一件事是 Python 中的打印函数很可能只是借用了对字典内容的引用。当它试图打印字典(或以任何其他方式访问其内容)时,后台线程出现并用新条目替换旧条目。这会导致旧条目的引用计数减少,然后垃圾收集器会删除该对象。但是,打印函数仍在尝试使用导致错误的旧引用。

我发现有趣的一点是,我可以通过仅更改字典中键的名称来更改扩展出现分段错误的速度。

是否有人对问题可能有任何见解?有没有更好的方法来创建扩展并仍然具有我想要的属性?

我相信我已经找到了段错误的原因。后台线程在未获得全局解释器锁 (GIL) 的情况下修改解释器的状态。这确实会导致解释器以意想不到的方式运行。

为了解决这个问题,我首先在模块初始化函数中调用函数 PyEval_InitThreads()。接下来要做的是在后台线程中包含任何使用 Python C API 和函数 PyGILState_Ensure() 和 PyGILState_Release() 的指令。下面是使用此修复程序修改后的源代码。

#include <Python.h>
#include <pthread.h>

static PyObject *module;
static PyObject *pyitem_error;
static PyObject *item;
static PyObject *item_handle;
static pthread_t thread;

void *stuff(void *param)
{
    int garbage = 0;
    PyObject *size;
    PyObject *value;
    PyGILState_STATE state;  // Needed for PyGILState_Ensure() and PyGILState_Release()

    while(1)
    {
        // Obtain the GIL
        state = PyGILState_Ensure();

        size = NULL;
        size = Py_BuildValue("{s:i,s:i}", "l", garbage, "w", garbage);
        if(size == NULL)
        {
            goto error;
        }

        value = NULL;            
        value = Py_BuildValue("{s:i,s:O}", "h", garbage, "base", size);
        if(value == NULL)
        {
            goto error;
        }

        PyDict_SetItemString(item, "dim", value);

        error:
        Py_XDECREF(size);
        Py_XDECREF(value);

        // Release the GIL
        PyGILState_Release(state);

        garbage++;              
    }

    return NULL;
}

static PyMethodDef pyitem_methods[] =
{
    {NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC initpyitem(void)
{
    module = Py_InitModule("pyitem", pyitem_methods);
    Py_INCREF(module);

    pyitem_error = PyErr_NewException("pyitem.error", NULL, NULL);
    Py_INCREF(pyitem_error);
    PyModule_AddObject(module, "error", pyitem_error);

    item = PyDict_New();
    item_handle = PyDictProxy_New(item);
    Py_INCREF(item_handle);
    PyModule_AddObject(module, "item", item_handle);

    // Initialize Global Interpreter Lock (GIL)
    PyEval_InitThreads();

    pthread_create(&thread, NULL, stuff, NULL);
}

扩展现在运行时没有任何分段错误。