28

基本上,对于应该调用的确切时间以及需要哪些伴随的 API 调用,似乎存在巨大的混淆/歧义。PyEval_InitThreads()不幸的是,官方 Python 文档非常含糊。关于这个话题, stackoverflow上已经有很多问题了,事实上,我个人已经问过一个与这个问题几乎相同的问题,所以如果这个问题被关闭,我不会特别惊讶;但考虑到这个问题似乎没有明确的答案。(遗憾的是,我没有快速拨号的 Guido Van Rossum。)

首先,让我们在这里定义问题的范围:我想做什么? 嗯...我想用 C 编写一个 Python 扩展模块,它将:

  1. pthread使用C 中的 API生成工作线程
  2. 从这些 C 线程中调用 Python 回调

好的,让我们从 Python 文档本身开始。Python 3.2 文档说:

无效 PyEval_InitThreads()

初始化并获取全局解释器锁。它应该在创建第二个线程或参与任何其他线程操作(例如 PyEval_ReleaseThread(tstate))之前在主线程中调用。在调用 PyEval_SaveThread() 或 PyEval_RestoreThread() 之前不需要它。

所以我的理解是:

  1. 任何产生线程的 C 扩展模块必须 PyEval_InitThreads()在产生任何其他线程之前从主线程调用
  2. 调用PyEval_InitThreads锁定 GIL

所以常识会告诉我们,任何创建线程的 C 扩展模块都必须调用PyEval_InitThreads(),然后释放全局解释器锁。好吧,看起来很简单。表面上看,所需要的只是以下代码:

PyEval_InitThreads(); /* initialize threading and acquire GIL */
PyEval_ReleaseLock(); /* Release GIL */

似乎很容易......但不幸的是,Python 3.2 文档PyEval_ReleaseLock弃用。相反,我们应该使用PyEval_SaveThread它来释放 GIL:

PyThreadState* PyEval_SaveThread()

释放全局解释器锁(如果已创建并启用线程支持)并将线程状态重置为 NULL,返回之前的线程状态(非 NULL)。如果锁已创建,则当前线程必须已获取它。

呃...好吧,所以我猜一个 C 扩展模块需要说:

PyEval_InitThreads();
PyThreadState* st = PyEval_SaveThread();


事实上,这正是这个stackoverflow答案所说的。除非我在实践中实际尝试过,否则当我导入扩展模块时,Python 解释器会立即出现段错误。好的。


好的,所以现在我放弃了官方的 Python 文档并转向谷歌。所以,这个随机的博客声称你需要从扩展模块做的就是调用PyEval_InitThreads(). 当然,PyEval_InitThreads()获得 GIL 的文档声称,事实上,快速检查PyEval_InitThreads()inceval.c的源代码表明它确实调用了内部函数take_gil(PyThreadState_GET());

所以PyEval_InitThreads() 肯定会获得 GIL。我会认为你绝对需要在调用PyEval_InitThreads().   但是怎么做? PyEval_ReleaseLock()已弃用,PyEval_SaveThread()只是莫名其妙的段错误。

好的......所以也许出于某种目前我无法理解的原因,C 扩展模块不需要发布 GIL。我试过了......并且,正如预期的那样,一旦另一个线程尝试获取 GIL(使用PyGILState_Ensure),程序就会因死锁而挂起。所以是的......你确实需要在调用PyEval_InitThreads().

再次,问题是:调用后如何释放 GIL PyEval_InitThreads()

更一般地说:C 扩展模块到底需要做什么才能安全地从工作 C 线程调用 Python 代码?

4

7 回答 7

15

您的理解是正确的:调用PyEval_InitThreads确实获得了 GIL。在正确编写的 Python/C 应用程序中,这不是问题,因为 GIL 会及时解锁,无论是自动还是手动。

如果主线程继续运行 Python 代码,没有什么特别的,因为 Python 解释器会在执行了一些指令后自动放弃 GIL(允许另一个线程获取它,它将再次放弃它,等等在)。此外,每当 Python 即将调用阻塞系统调用时,例如从网络读取或写入文件,它都会在调用周围释放 GIL。

这个答案的原始版本几乎到此结束。但是还有一件事需要考虑:嵌入场景。

在嵌入 Python 时,主线程通常会初始化 Python 并继续执行其他与 Python 无关的任务。在那种情况下,没有任何东西会自动释放 GIL,所以这必须由线程本身完成。这绝不是特定于调用的调用PyEval_InitThreads,它应该适用于使用获得的 GIL 调用的所有 Python/C 代码。

例如,main()可能包含如下代码:

Py_Initialize();
PyEval_InitThreads();

Py_BEGIN_ALLOW_THREADS
... call the non-Python part of the application here ...
Py_END_ALLOW_THREADS

Py_Finalize();

如果您的代码手动创建线程,则它们需要在执行任何与 Python 相关的操作之前获取 GIL,即使是简单的Py_INCREF. 为此,请使用以下命令:

// Acquire the GIL
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();

... call Python code here ...

// Release the GIL. No Python API allowed beyond this point.
PyGILState_Release(gstate);
于 2013-03-18T07:16:42.823 回答
7

执行 C/Python API 时有两种多线程方法。

1.使用相同的解释器执行不同的线程——我们可以执行一个Python解释器并在不同的线程上共享相同的解释器。

编码如下。

main(){     
//initialize Python
Py_Initialize();
PyRun_SimpleString("from time import time,ctime\n"
    "print 'In Main, Today is',ctime(time())\n");

//to Initialize and acquire the global interpreter lock
PyEval_InitThreads();

//release the lock  
PyThreadState *_save;
_save = PyEval_SaveThread();

// Create threads.
for (int i = 0; i<MAX_THREADS; i++)
{   
    hThreadArray[i] = CreateThread
    //(...
        MyThreadFunction,       // thread function name
    //...)

} // End of main thread creation loop.

// Wait until all threads have terminated.
//...
//Close all thread handles and free memory allocations.
//...

//end python here
//but need to check for GIL here too
PyEval_RestoreThread(_save);
Py_Finalize();
return 0;
}

//the thread function

DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{
//non Pythonic activity
//...

//check for the state of Python GIL
PyGILState_STATE gilState;
gilState = PyGILState_Ensure();
//execute Python here
PyRun_SimpleString("from time import time,ctime\n"
    "print 'In Thread Today is',ctime(time())\n");
//release the GIL           
PyGILState_Release(gilState);   

//other non Pythonic activity
//...
return 0;
}
  1. 另一种方法是,我们可以在主线程中执行一个 Python 解释器,并且对于每个线程,我们可以给它自己的子解释器。因此,每个线程都使用自己独立的所有导入模块的独立版本运行,包括基本模块 - builtins、__main__ 和 sys。

代码如下

int main()
{

// Initialize the main interpreter
Py_Initialize();
// Initialize and acquire the global interpreter lock
PyEval_InitThreads();
// Release the lock     
PyThreadState *_save;
_save = PyEval_SaveThread();


// create threads
for (int i = 0; i<MAX_THREADS; i++)
{

    // Create the thread to begin execution on its own.

    hThreadArray[i] = CreateThread
    //(...

        MyThreadFunction,       // thread function name
    //...);   // returns the thread identifier 

} // End of main thread creation loop.

  // Wait until all threads have terminated.
WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE);

// Close all thread handles and free memory allocations.
// ...


//end python here
//but need to check for GIL here too
//re capture the lock
PyEval_RestoreThread(_save);
//end python interpreter
Py_Finalize();
return 0;
}

//the thread functions
DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{
// Non Pythonic activity
// ...

//create a new interpreter
PyEval_AcquireLock(); // acquire lock on the GIL
PyThreadState* pThreadState = Py_NewInterpreter();
assert(pThreadState != NULL); // check for failure
PyEval_ReleaseThread(pThreadState); // release the GIL


// switch in current interpreter
PyEval_AcquireThread(pThreadState);

//execute python code
PyRun_SimpleString("from time import time,ctime\n" "print\n"
    "print 'Today is',ctime(time())\n");

// release current interpreter
PyEval_ReleaseThread(pThreadState);

//now to end the interpreter
PyEval_AcquireThread(pThreadState); // lock the GIL
Py_EndInterpreter(pThreadState);
PyEval_ReleaseLock(); // release the GIL

// Other non Pythonic activity
return 0;
}

需要注意的是,全局解释器锁仍然存在,尽管为每个线程提供了单独的解释器,但当涉及到 python 执行时,我们仍然一次只能执行一个线程。GILPROCESS唯一的,因此尽管为每个线程提供了唯一的子解释器,但我们不能同时执行线程

资料来源:在主线程中执行 Python 解释器,对于每个线程,我们可以提供自己的子解释器

多线程教程 (msdn)

于 2017-03-08T09:36:06.083 回答
6

我已经看到了与您类似的症状:如果我只调用 PyEval_InitThreads(),则会出现死锁,因为我的主线程再也不会从 Python 调用任何东西,如果我无条件地调用 PyEval_SaveThread() 之类的东西,则会出现段错误。症状取决于 Python 的版本和情况:我正在开发一个嵌入 Python 的插件,用于可以作为 Python 扩展的一部分加载的库。因此,代码需要独立运行,无论它是否由 Python 作为 main 加载。

以下适用于 python2.7 和 python3.4,以及我的库在 Python 内和 Python 之外运行。在主线程中执行的插件初始化例程中,我运行:

  Py_InitializeEx(0);
  if (!PyEval_ThreadsInitialized()) {
    PyEval_InitThreads();
    PyThreadState* mainPyThread = PyEval_SaveThread();
  }

(mainPyThread 实际上是一些静态变量,但我认为这并不重要,因为我再也不需要使用它了)。

然后我使用 pthreads 创建线程,在每个需要访问 Python API 的函数中,我使用:

  PyGILState_STATE gstate;
  gstate = PyGILState_Ensure();
  // Python C API calls
  PyGILState_Release(gstate);
于 2015-06-17T10:20:50.423 回答
2

引用上面的:

简短的回答:你不应该关心在调用 PyEval_InitThreads 之后释放 GIL ......

现在,要获得更长的答案:

我将我的答案限制为关于 Python 扩展(而不是嵌入 Python)。如果我们只是扩展 Python,那么您模块的任何入口点都来自 Python。根据定义,这意味着我们不必担心从非 Python 上下文调用函数,这使事情变得更简单。

如果线程还没有被初始化,那么我们知道没有 GIL(没有线程 == 不需要锁定),因此“当不知道哪个线程(如果有)当前具有全局解释器锁定”不适用。

if (!PyEval_ThreadsInitialized())
{
    PyEval_InitThreads();
}

在调用 PyEval_InitThreads() 之后,会创建一个 GIL 并将其分配给我们的线程,也就是当前运行 Python 代码的线程。所以一切都很好。

现在,就我们自己启动的worker“C”线程而言,它们在运行相关代码之前需要请求GIL:因此它们的常用方法如下:

// Do only non-Python things up to this point
PyGILState_STATE state = PyGILState_Ensure();
// Do Python-things here, like PyRun_SimpleString(...)
PyGILState_Release(state);
// ... and now back to doing only non-Python things

与正常使用扩展相比,我们不必担心死锁。当我们进入我们的函数时,我们可以控制 Python,所以要么我们没有使用线程(因此,没有 GIL),要么 GIL 已经分配给了我们。当我们通过退出函数将控制权交还给 Python 运行时时,正常的处理循环将检查 GIL 并根据需要手动控制其他请求对象:包括通过 PyGILState_Ensure() 的工作线程。

所有这些读者可能已经知道了。然而,“证据就在布丁里”。我发布了一个文档很少的示例,我今天编写该示例是为了让自己了解实际行为是什么,以及事情是否正常工作。 GitHub 上的示例源代码

我通过该示例学习了几件事,包括 CMake 与 Python 开发的集成、与上述两者的 SWIG 集成以及与扩展和线程的 Python 行为。尽管如此,该示例的核心仍然允许您:

  • 加载模块——'import annoy'
  • 加载零个或多个执行 Python 操作的工作线程 - 'annoy.annoy(n)'
  • 清除所有工作线程——'annon.annoy(0)'
  • 在应用程序退出时提供线程清理(在 Linux 上)

...所有这一切都没有任何崩溃或段错误。至少在我的系统上(Ubuntu Linux w/GCC)。

于 2015-09-16T19:45:44.893 回答
1

调用 PyEval_SaveThread 的建议有效

PyEval_InitThreads();
PyThreadState* st = PyEval_SaveThread();

但是,为了防止在导入模块时崩溃,请确保要导入的 Python API 受到保护

PyGILState_Ensure 和 PyGILState_Release

例如

PyGILState_STATE gstate = PyGILState_Ensure();
PyObject *pyModule_p = PyImport_Import(pyModuleName_p);
PyGILState_Release(gstate);
于 2014-01-22T09:50:55.817 回答
1

我也对这个问题感到困惑。以下代码巧合。

Py_InitializeEx(0);
    if (!PyEval_ThreadsInitialized()) {
    PyEval_InitThreads();
    PyThreadState* mainPyThread = PyEval_SaveThread();
}

我的主线程做一些 python 运行时的初始工作,并创建其他 pthread 来处理任务。我有一个更好的解决方法。在主线程中:

if (!PyEval_ThreadsInitialized()){
    PyEval_InitThreads();
}
//other codes
while(alive) {
    Py_BEGIN_ALLOW_THREADS
    sleep or other block code
    Py_END_ALLOW_THREADS
}
于 2016-11-12T05:39:26.503 回答
0

您不需要在扩展模块中调用它。这是为了初始化解释器,如果你的 C-API 扩展模块正在被导入,这已经完成了。此接口将由嵌入应用程序使用。

PyEval_InitThreads 什么时候应该被调用?

于 2013-03-18T06:40:22.090 回答