【问题标题】:Global Interpreter Lock and access to data (eg. for NumPy arrays)全局解释器锁定和数据访问(例如,用于 NumPy 数组)
【发布时间】:2012-01-11 18:36:25
【问题描述】:

我正在为 Python 编写一个 C 扩展,它应该在对数据进行操作时释放全局解释器锁。我想我已经很好地理解了 GIL 的机制,但是还有一个问题:我可以在线程不拥有 GIL 的情况下访问 Python 对象中的数据吗?例如,我想从 C 函数中的(大)NumPy 数组中读取数据,同时我仍然希望允许其他线程在其他 CPU 内核上执行其他操作。 C函数应该

  • 使用Py_BEGIN_ALLOW_THREADS 发布 GIL
  • 在不使用 Python 函数的情况下读取和处理数据
  • 甚至将数据写入之前构建的 NumPy 数组
  • 使用Py_END_ALLOW_THREADS 重新获取 GIL

这样安全吗?当然,其他线程不应该更改 C 函数使用的变量。但也许有一个隐藏的错误来源:Python 解释器是否可以移动一个对象,例如。通过某种垃圾收集,而 C 函数在单独的线程中处理它?

为了用一个最小的例子来说明这个问题,请考虑下面的(最小但完整的)代码。用

编译它(在Linux上)
gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -fPIC -I/usr/lib/pymodules/python2.7/numpy/core/include -I/usr/include/python2.7 -c gilexample.c -o gilexample.o
gcc -pthread -shared gilexample.o -o gilexample.so

并在 Python 中使用

对其进行测试
import gilexample
gilexample.sum([1,2,3])

Py_BEGIN_ALLOW_THREADSPy_END_ALLOW_THREADS 之间的代码安全吗?它访问 Python 对象的内容,我不想在内存中复制(可能很大)数组。

#include <Python.h>
#include <numpy/arrayobject.h>

// The relevant function
static PyObject * sum(PyObject * const self, PyObject * const args) {
  PyObject * X;
  PyArg_ParseTuple(args, "O", &X);
  PyObject const * const X_double = PyArray_FROM_OTF(X, NPY_DOUBLE, NPY_ALIGNED);
  npy_intp const size = PyArray_SIZE(X_double);
  double * const data = (double *) PyArray_DATA(X_double);
  double sum = 0;

  Py_BEGIN_ALLOW_THREADS // IS THIS SAFE?

  npy_intp i;
  for (i=0; i<size; i++)
    sum += data[i];

  Py_END_ALLOW_THREADS

  Py_DECREF(X_double);
  return PyFloat_FromDouble(sum);
}

// Python interface code
// List the C methods that this extension provides.
static PyMethodDef gilexampleMethods[] = {
  {"sum", sum, METH_VARARGS},
  {NULL, NULL, 0, NULL}     /* Sentinel - marks the end of this structure */
};

// Tell Python about these methods.
PyMODINIT_FUNC initgilexample(void)  {
  (void) Py_InitModule("gilexample", gilexampleMethods);
  import_array();  // Must be present for NumPy.
}

【问题讨论】:

  • 我过去做过这样的事情,我发现最简单的方法是使用ctypes 调用你的C 函数。为您的 C 函数提供一个纯 C 接口,而不引用 Python 或 NumPy,并在 Python 中编写简单的包装器,接受 NumPy 数组并将它们转换为适当的 C 参数。我在this answer 中举例说明了如何做到这一点。
  • @Sven:你知道ctypes 是否在内存中制作了数组的工作副本吗? (1) 是的。在这种情况下,我不想要它,因为我正在处理大型输入数组。 (2) 否。那么我的问题是否可以解除 GIL 仍然有效。但是,在情况 (2) 中,ctypes 行为将暗示解除 GIL 可能没有问题,在不使用 ctypes 的代码中也是如此。有谁知道(1)或(2)是否成立?
  • 不,ctypes 不会复制数组。它会为您释放 GIL,因此您不必关心它。使用ctypes 的优点是简单——您必须在 Python 中时从 NumPy 数组中提取所有必要的元信息,并且 GIL 会适时发布。我使用这种方法从多个线程同时访问 NumPy 数组中的数据。 (请注意,对同一内存的并发写访问永远不会保存。)
  • 谢谢,Sven,这是迄今为止对我最有帮助的评论,即使它与最初的问题有点偏离 ctypes。如果我可以将评论标记为“已接受的答案”,就是这样。我想知道ctypes 是否比上面的显式小示例在内部保护内存方面做得更多,但对于一个实际的解决方案,现在知道ctypes 存在一个好方法就足够了。
  • 我把它写成评论而不是答案,因为它离题了。 ctypes 没有做任何事情来保护内存。将 NumPy 数组作为参数传递给 C 函数的代码实际上是在 NumPy 中,而不是在 ctypes 中——ctypes 是标准库的一部分,不知道 NumPy。代码所做的只是提取指向基础数据的指针并将其传递给ctypes,因此ctypes 甚至不知道数据的大小。稍后我会写一个更中肯的答案。

标签: python numpy python-c-api


【解决方案1】:

这样安全吗?

严格来说,不。我认为您应该将呼叫移至无 GIL 块之外的 PyArray_SIZEPyArray_DATA;如果你这样做,你将只对 C 数据进行操作。您可能还想在进入无 GIL 块之前增加对象的引用计数,然后再减少它。

编辑后,它应该是安全的。之后不要忘记减少引用计数。

【讨论】:

  • 感谢您对PyArray_SIZEPyArray_DATA 的评论。那是个错误。我编辑了我的问题并将命令移到了释放 GIL 的块之外。
  • 现在:修订版中的代码安全吗?你能详细说明为什么增加引用计数会改变事情吗?
  • @Daniel:可能需要引用计数(尽管在这种情况下我并不完全确定),因为没有其他线程必须释放数组。
  • 我检查了引用计数:PyArray_FROM_OTF 已经增加了引用计数,因此无需手动进行。然而,这让我注意到Py_DECREF 在我的示例代码末尾是有序的;否则会有内存泄漏。谢谢,拉尔斯曼!我更正了我原来的帖子。因此,作为对您原始评论的回答:如果我手动增加了引用计数器,这将是不必要的并且不会改变任何东西,因为引用计数器至少为 1,因此该对象不会被释放。
【解决方案2】:

我可以在线程不拥有 GIL 的情况下访问 Python 对象中的数据吗?

不,你不能。

【讨论】:

  • 这是否意味着我总是必须复制 NumPy 数组的内容,即使我只想在线程中读取数据?我希望有办法解决它!有什么建议吗?
猜你喜欢
  • 2011-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多