CPython - 在主线程中锁定GIL

16 投票
1 回答
2266 浏览
提问于 2025-04-18 11:39

关于CPython线程支持的文档让人感到很困惑,内容既矛盾又稀少。

一般来说,大家都同意,嵌入Python的多线程C应用程序在调用Python解释器之前,必须先获取全局解释器锁(GIL)。通常,这个过程是通过以下方式完成的:

PyGILState_STATE s = PyGILState_Ensure();

/* do stuff with Python */

PyGILState_Release(s);

文档中对此有很明确的说明:https://docs.python.org/2/c-api/init.html#non-python-created-threads

然而,实际上,让一个嵌入Python的多线程C程序顺利运行却是另一回事。即使你完全按照文档操作,仍然会遇到很多奇怪的问题和意外情况。

举个例子,似乎在后台,Python会区分“主线程”(我猜就是调用Py_Initialize的那个线程)和其他线程。具体来说,在“主线程”中尝试获取GIL并运行Python代码时,我的尝试总是失败——(至少在Python 3.x中),程序会因为Fatal Python error: drop_gil: GIL is not locked的错误信息而中止,这真是让人无奈,因为GIL明明是被锁住的啊!

示例:

int main()
{
    Py_Initialize();
    PyEval_InitThreads();
    PyEval_ReleaseLock();

    assert(PyEval_ThreadsInitialized());

    PyGILState_STATE s = PyGILState_Ensure();

    const char* command = "x = 5\nfor i in range(0,10): print(x*i)";
    PyRun_SimpleString(command);

    PyGILState_Release(s);
    Py_Finalize();

    return 0;
}

这个简单的程序会因为“GIL没有被锁住”的错误而中止,尽管我明明已经锁住了它。然而,如果我创建了另一个线程,并在那个线程中尝试获取GIL,一切就正常了。

所以CPython似乎有一个(未记录的)“主线程”概念,这个线程和C创建的其他线程有些不同。

问题:这个问题在文档中有说明吗?有没有人有经验可以解释一下获取GIL的具体规则,以及在“主线程”和子线程之间的区别是否会影响这个过程?

附注:我还注意到PyEval_ReleaseLock是一个已弃用的API调用,但我没有看到任何实际可用的替代方案。如果在调用PyEval_InitThreads后不调用PyEval_ReleaseLock,你的程序会立即挂掉。然而,文档中提到的更新替代方案PyEval_SaveThread在我这里从来没有成功过——如果我在“主线程”中调用它,程序会立刻崩溃。

1 个回答

5

这个简单的程序出现了“GIL没有被锁定”的错误,尽管我明明已经锁定了它。

你确实锁定了GIL,但接着你在PyGILState_Release中释放了它,这意味着你在没有持有GIL的情况下调用了Py_Finalize,这就出问题了。

有没有人有过经验,可以帮我弄清楚获取GIL的具体规则是什么?

理解GIL的一个好方法是,调用PyEval_InitThreads()后,总会有人持有GIL,或者只是暂时释放它,使用Py_BEGIN_ALLOW_THREADSPy_END_ALLOW_THREADS。有关类似困惑的详细讨论,可以参考这个回答

在你的情况下,正确编写示例程序的方式如下:

#include <Python.h>

static void various()
{
    // here we don't have the GIL and can run non-Python code without
    // blocking Python

    PyGILState_STATE s = PyGILState_Ensure();
    // from this line, we have the GIL, and we can run Python code

    const char* command = "x = 5\nfor i in range(0,10): print(x*i)";
    PyRun_SimpleString(command);

    PyGILState_Release(s);
    // from this line, we no longer have the GIL
}

int main()
{
    Py_Initialize();
    PyEval_InitThreads();
    // here we have the GIL
    assert(PyEval_ThreadsInitialized());

    Py_BEGIN_ALLOW_THREADS
    // here we no longer have the GIL, although various() is free to
    // (temporarily) re-acquire it
    various();
    Py_END_ALLOW_THREADS

    // here we again have the GIL, which is why we can call Py_Finalize 
    Py_Finalize();

    // at this point the GIL no longer exists
    return 0;
}

撰写回答