在Grand Central Dispatch中使用非Python线程嵌入Python会导致Py_Finalize错误

时间:2015-05-13 18:53:16

标签: python python-3.x grand-central-dispatch

这发生在Python 3.4.3来自非python创建的线程:

https://docs.python.org/3.4/c-api/init.html#non-python-created-threads

在SO上有几个问题可以从C / C ++应用程序中查看类似的问题。

AssertionError (3.X only) when calling Py_Finalize with threads

Fatal error during Py_Finalize in embedded Python application

PyEval_InitThreads in Python 3: How/when to call it? (the saga continues ad nauseum)

但是没有一个专门处理Grand Central Dispatch。我不知道它是否重要,因为它可能只是引擎盖下的线程。

然而,试图从这些帖子中应用知识仍然会引起我的问​​题。

所以这就是我目前所处的位置,我有一个代表我的Python Runtime的obj-c类,我有以下相关方法:

- (void)initialize
{
    Py_Initialize();
    PyEval_InitThreads();

    PyObject* sysPath = PySys_GetObject((char*)"path");

    for(NSString * path in self.pythonPath){
        PyList_Append(sysPath, objc_convert_string(path));
    }

    // not calling PyEval_SaveThread 
    // causes beginTask below to die on 
    // PyEval_AcquireThread

    // self.threadState = PyThreadState_Get();
    // release the GIL, this shouldn't need to be
    // done, as I understand it, 
    // but as the comment above states, if I don't
    // beginTask will fail at PyEval_AcquireThread
    self.threadState = PyEval_SaveThread();
    self.running = YES;
}

这就是我初始化Python的方式。然后我通过以下方式调用python命令:

- (void)beginTask:(nonnull void (^)(void))task completion:(nullable void (^)(void))completion
{
    dispatch_async(self.pythonQueue, ^{

        PyInterpreterState * mainInterpreterState = self.threadState->interp;
        PyThreadState * myThreadState = PyThreadState_New(mainInterpreterState);
        PyEval_AcquireThread(myThreadState);

        // Perform any Py_* related functions here
        task();

        PyEval_ReleaseThread(PyThreadState_Get());

        if (completion){
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }

    });
}

在执行长时间运行操作的情况下,我有一些代码可以完成Jinja模板的一些代码,从而保存到文件系统。我想在完成后清理,所以我打电话给Py_Finalize然后重新初始化,使用上面的方法输入我的问题:

- (void)finalize
{
    PyEval_RestoreThread(self.threadState);

    // Problems here
    Py_Finalize();

    self.running = NO;
}

这会导致Python中出现以下错误:

Exception ignored in: <module 'threading' from '/Users/blitz6/Library/Developer/Xcode/DerivedData/Live-cblwxzmbsdpraeebpregeuhikwzh/Build/Products/Debug/LivePython.framework/Resources/python3.4/lib/python3.4/threading.py'>
Traceback (most recent call last):
  File "/Users/blitz6/Library/Developer/Xcode/DerivedData/Live-cblwxzmbsdpraeebpregeuhikwzh/Build/Products/Debug/LivePython.framework/Resources/python3.4/lib/python3.4/threading.py", line 1296, in _shutdown
    _main_thread._delete()
  File "/Users/blitz6/Library/Developer/Xcode/DerivedData/Live-cblwxzmbsdpraeebpregeuhikwzh/Build/Products/Debug/LivePython.framework/Resources/python3.4/lib/python3.4/threading.py", line 1019, in _delete
    del _active[get_ident()]
KeyError: 140735266947840

我尝试过几种不同的方法,包括在PyGILState_Ensure()中使用PyGILState_Release(myState);beginTask

- (void)beginTask:(nonnull void (^)(void))task completion:(nullable void (^)(void))completion
{

    dispatch_async(self.pythonQueue, ^{

        PyGILState_STATE state = PyGILState_Ensure();

        task();

        PyGILState_Release(state);

        if (completion){
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }
    });
}

但是这会导致上面的finalize方法出现此错误:

Exception ignored in: <module 'threading' from '/Users/blitz6/Library/Developer/Xcode/DerivedData/Live-cblwxzmbsdpraeebpregeuhikwzh/Build/Products/Debug/LivePython.framework/Resources/python3.4/lib/python3.4/threading.py'>
Traceback (most recent call last):
  File "/Users/blitz6/Library/Developer/Xcode/DerivedData/Live-cblwxzmbsdpraeebpregeuhikwzh/Build/Products/Debug/LivePython.framework/Resources/python3.4/lib/python3.4/threading.py", line 1289, in _shutdown
    assert tlock.locked()
AssertionError: 

所以我几乎坚持:如何Py_Finalize没有出现某种错误。显然,我不理解某些事情。我也可以通过xcode确认,当我点击断点时,我的dispatch_async块正在从另一个不是主线程的线程运行。

更新

修补一下,我发现了,这个:

PyObject* module = PyImport_ImportModule("requests");

当我在另一个线程上时,在我Py_Finalize

时会导致此错误
Exception ignored in: <module 'threading' from '/Users/blitz6/Library/Developer/Xcode/DerivedData/Live-cblwxzmbsdpraeebpregeuhikwzh/Build/Products/Debug/LivePython.framework/Resources/python3.4/lib/python3.4/threading.py'>
Traceback (most recent call last):
  File "/Users/blitz6/Library/Developer/Xcode/DerivedData/Live-cblwxzmbsdpraeebpregeuhikwzh/Build/Products/Debug/LivePython.framework/Resources/python3.4/lib/python3.4/threading.py", line 1296, in _shutdown
    _main_thread._delete()
  File "/Users/blitz6/Library/Developer/Xcode/DerivedData/Live-cblwxzmbsdpraeebpregeuhikwzh/Build/Products/Debug/LivePython.framework/Resources/python3.4/lib/python3.4/threading.py", line 1019, in _delete
    del _active[get_ident()]
KeyError: 140735266947840

如果我导入:

PyObject* module = PyImport_ImportModule("os");
OR
PyObject* module = PyImport_ImportModule("json");

一切都运行得很好。当我开始导入自己的模块时,我遇到了问题。

Py_Finalize内,wait_for_thread_shutdown();是我遇到此问题的地方。我想,根据评论,它与:

有关
  

/ *等到threading._shutdown完成,提供      首先导入了线程模块。      关闭例程将等到所有非守护进程      “线程”线程已经完成。 * /

具体在wait_for_thread_shutdown

PyThreadState *tstate = PyThreadState_GET();
PyObject *threading = PyMapping_GetItemString(tstate->interp->modules,
                                              "threading");
if (threading == NULL) {
    /* threading not imported */
    PyErr_Clear();
    return;
}

tstate返回 NULL ,但threading不是 NULL ,它会跳过PyErr_Clear()代码路径并执行:

result = _PyObject_CallMethodId(threading, &PyId__shutdown, "");
if (result == NULL) {
    PyErr_WriteUnraisable(threading);
}
else {
    Py_DECREF(result);
}
Py_DECREF(threading);

如果我只是:

PyObject* module = PyImport_ImportModule("json");

然后,PyErr_WriteUnraisable(threading);wait_for_thread_shutdown

中执行
result = _PyObject_CallMethodId(threading, &PyId__shutdown, "");
if (result == NULL) {
    PyErr_WriteUnraisable(threading);
}

2 个答案:

答案 0 :(得分:1)

如前所述,我正在考虑我的代码而我正在考虑嵌入式Python解释器错误。它与GCD无关,而且与我对嵌入式Python的某些方面的误解有关。我可能仍然有误解,但下面的逻辑符合我对我认为应该做的事情的期望,我的错误消失了。

我试图做的是执行一次性任务,而不是保留任何东西。认为我需要在这里穿线是让我沮丧的原因。

我正在做的是获取GIL,然后导入一些使用threading模块的python代码。当你这样做时,解释器会注册你引入了线程模块,就像你Py_Finalize它跳过一些箍时一样,以确保你可能存在或不存在的所有子线程都被关闭。我实际上正在从它下面拉出地毯,这导致了我的错误。相反,我所做的工作更有利于Py_NewInterpreter。当您致电threading但与自身隔离时,它会与Py_Finalize完全相同的Py_EndInterpreter关机程序。

所以我的最终GCD一次性完成代码如下:

<强>初始化

- (void)initialize
{
    Py_Initialize();
    PyEval_InitThreads();

    [self updateSysPath];

    // Release the GIL
    self.threadState = PyEval_SaveThread();
    self.running = YES;

}

任务执行

- (void)beginTask:(nonnull void (^)(Runtime * __nonnull))task completion:(nullable void (^)(Runtime * __nonnull))completion
{
    dispatch_async(self.pythonQueue, ^{

        PyInterpreterState * mainInterpreterState = self.threadState->interp;
        PyThreadState * taskState = PyThreadState_New(mainInterpreterState);

        // Acquire the GIL
        PyEval_AcquireThread(taskState);

        PyThreadState* tstate = Py_NewInterpreter();
        [self updateSysPath];

        task(self);

        // when Py_EndInterpreter is called, the current 
        // thread state is set to NULL, 
        // so we need to put ourselves back on
        // the taskState, and release the GIL
        Py_EndInterpreter(tstate);
        PyThreadState_Swap(taskState);

        // release the GIL
        PyEval_ReleaseThread(taskState);

        // cleanup
        PyThreadState_Clear(taskState);
        PyThreadState_Delete(taskState);

        if (completion){
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(self);
            });
        }
    });
}

<强>最后确定

- (void)finalize
{
    // acquire the GIL
    PyEval_RestoreThread(self.threadState);
    Py_Finalize();
    self.running = NO;
}

答案 1 :(得分:0)

你说:

  

但是没有一个专门处理Grand Central Dispatch。一世   不知道它是否重要,因为它可能只是线程   引擎盖

是的,最终,GCD块必须在一个OS线程或另一个OS线程上执行,但GCD队列在单个线程之上的抽象,并且(主队列除外)make根本没有关于线程亲和力的承诺。换句话说,没有办法保证提交的块在同一个线程或任何特定的线程上执行,也不明智地对它们最终执行的线程做出任何假设。

最安全,最直接的方法可能是显式管理操作系统线程(通过NSThreadpthreads API)计划用于运行Python代码。 GCD中的线程管理在一天结束时是一个私有的实现细节,这意味着即使你设法得到现在有效的东西,Apple也可以对GCD中的线程管理进行一些微妙的改动,并打破你的应用程序。将来

总之,GCD队列可能不适合这项工作。

我最近发布了another answer describing a way to manage a private thread for wrapping a third-party library that demanded thread affinity。你可能会感兴趣。