【问题标题】:Embedding Python, using Non-Python threads in Grand Central Dispatch causes Py_Finalize errors嵌入 Python,在 Grand Central Dispatch 中使用非 Python 线程会导致 Py_Finalize 错误
【发布时间】:2015-05-13 18:53:16
【问题描述】:

这发生在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 运行时的 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

我尝试了几种不同的方法来处理这个问题,包括在beginTask 中使用PyGILState_Ensure()PyGILState_Release(myState);

- (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_Finalizewait_for_thread_shutdown(); 内部是我遇到这个问题的地方。我想,根据 cmets 的说法,它与:

/* 等到 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 返回 NULLthreading 不是 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);
}

【问题讨论】:

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


    【解决方案1】:

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

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

    我正在做的是获取 GIL,然后导入一些使用 threading 模块的 python 代码。当你这样做时,解释器注册你已经引入线程模块,当你 Py_Finalize 它跳过一些箍以确保你可能存在或可能不存在的所有子线程都被关闭。我有效地将地毯从它下面拉出来,这导致了我的错误。相反,我需要做的工作更有利于Py_NewInterpreter。当您调用Py_EndInterpreter 时,它运行与Py_Finalize 完全相同的threading 关闭过程,但它是独立于自身的。

    所以我最终的 GCD one-and-done 代码如下所示:

    初始化

    - (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;
    }
    

    【讨论】:

      【解决方案2】:

      你说:

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

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

      最安全、最直接的方法可能是让您显式管理您计划用于的操作系统线程(通过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。您可能对此感兴趣。

      【讨论】:

      • 这是一个不错的解决方案,我学到了一些东西!谢谢!!然而,在进行了更多挖掘之后,我发现我的问题实际上是由于考虑了我需要执行的代码和错误的 Python 解释器。就我而言,我正在执行 1-off 任务,所以我不需要担心线程关联性。该任务在 GCD 线程中运行,完成并永远完成。
      猜你喜欢
      • 2011-06-04
      • 1970-01-01
      • 2011-11-24
      • 1970-01-01
      • 2012-05-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多