Python 3中的PyEval_InitThreads:如何/何时调用?(这个故事无休无止)

31 投票
7 回答
13893 浏览
提问于 2025-04-17 19:28

基本上,大家对什么时候应该调用 PyEval_InitThreads() 这个函数,以及需要配合哪些其他的API调用,似乎有很大的困惑和不明确。可惜的是,官方的Python文档对此并没有给出清晰的解释。关于这个话题,已经有很多人在stackoverflow上提问了,实际上,我自己也曾经问过一个几乎一模一样的问题,所以如果这个问题被关闭为重复问题,我也不会感到特别惊讶;但考虑到这个问题似乎没有明确的答案,我就很无奈了。(可惜,我没有把Guido Van Rossum加到我的快速拨号里。)

首先,让我们明确一下问题的范围:我想做什么? 嗯……我想用C语言写一个Python扩展模块,它需要:

  1. 使用C语言的 pthread API 创建工作线程
  2. 在这些C线程中调用Python的回调函数

好的,那我们先看看Python的文档。Python 3.2文档上说:

void 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()ceval.c 中的源代码 确实显示它调用了内部函数 take_gil(PyThreadState_GET());

所以 PyEval_InitThreads() 肯定 会获取GIL。我想那么你肯定需要在调用 PyEval_InitThreads() 之后以某种方式释放GIL。但是怎么做呢? PyEval_ReleaseLock() 已经不推荐使用,而 PyEval_SaveThread() 却莫名其妙地崩溃。

好吧……所以也许出于某种我现在还不理解的原因,C扩展模块不需要释放GIL。我试过这样做……结果如我所料,一旦另一个线程尝试获取GIL(使用 PyGILState_Ensure),程序就会因为死锁而挂掉。所以,是的……你确实需要在调用 PyEval_InitThreads() 之后释放GIL。

所以再次提问:在调用 PyEval_InitThreads() 之后,如何释放GIL?

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

7 个回答

6

我遇到过和你类似的问题:如果我只调用 PyEval_InitThreads(),就会出现死锁,因为我的主线程之后再也不会调用任何 Python 的东西;而如果我不加条件地调用像 PyEval_SaveThread() 这样的函数,就会出现段错误。出现这些问题的原因和情况以及 Python 的版本有关:我正在开发一个插件,这个插件会把 Python 嵌入到一个可以作为 Python 扩展加载的库中。因此,这段代码需要能够独立运行,无论它是作为主程序被 Python 加载还是其他方式。

以下代码在 python2.7 和 python3.4 中都能正常工作,并且我的库在 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);
8

在执行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;
}

需要注意的是,全球解释器锁(GIL)依然存在。尽管给每个线程提供了独立的解释器,但在执行Python代码时,我们仍然只能一次执行一个线程。GIL唯一的,针对进程的,所以即使给每个线程提供了独特的子解释器,我们也无法实现线程的同时执行。

来源: 在主线程中执行Python解释器,并给每个线程分配自己的子解释器

多线程教程 (msdn)

17

你的理解是对的:调用 PyEval_InitThreads 的时候,确实会获取到全局解释器锁(GIL)。在一个写得正确的 Python/C 应用程序中,这并不是个问题,因为 GIL 会在适当的时候被释放,要么是自动释放,要么是手动释放。

如果主线程继续运行 Python 代码,那就没什么特别需要做的,因为 Python 解释器会在执行了一定数量的指令后自动释放 GIL(这样其他线程就可以获取到 GIL,然后再释放,依此类推)。另外,每当 Python 要执行一个会阻塞的系统调用,比如从网络读取数据或写入文件时,它会在调用前释放 GIL。

这个回答的原始版本基本上到这里就结束了。但还有一件事需要考虑:嵌入(embedding)场景。

在嵌入 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);

撰写回答