Python 3中的PyEval_InitThreads:如何/何时调用?(这个故事无休无止)
基本上,大家对什么时候应该调用 PyEval_InitThreads()
这个函数,以及需要配合哪些其他的API调用,似乎有很大的困惑和不明确。可惜的是,官方的Python文档对此并没有给出清晰的解释。关于这个话题,已经有很多人在stackoverflow上提问了,实际上,我自己也曾经问过一个几乎一模一样的问题,所以如果这个问题被关闭为重复问题,我也不会感到特别惊讶;但考虑到这个问题似乎没有明确的答案,我就很无奈了。(可惜,我没有把Guido Van Rossum加到我的快速拨号里。)
首先,让我们明确一下问题的范围:我想做什么? 嗯……我想用C语言写一个Python扩展模块,它需要:
- 使用C语言的
pthread
API 创建工作线程 - 在这些C线程中调用Python的回调函数
好的,那我们先看看Python的文档。Python 3.2文档上说:
void PyEval_InitThreads()
初始化并获取全局解释器锁。它应该在主线程中调用,之后再创建第二个线程或进行其他线程操作,比如
PyEval_ReleaseThread(tstate)
。在调用PyEval_SaveThread()
或PyEval_RestoreThread()
之前不需要调用它。
所以我理解的是:
- 任何创建线程的C扩展模块必须在主线程中调用
PyEval_InitThreads()
,然后才能创建其他线程 - 调用
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 个回答
我遇到过和你类似的问题:如果我只调用 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);
在执行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;
}
- 另一种方法是,我们可以在主线程中执行一个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是唯一的,针对进程的,所以即使给每个线程提供了独特的子解释器,我们也无法实现线程的同时执行。
你的理解是对的:调用 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);