3

It's recommended to wrap pure C functions that block on IO, or even pure C functions that spend substantial time in CPU, in Py_BEGIN_ALLOW_THREADS and Py_END_ALLOW_THREADS blocks.

Is there any harm to wrapping pretty much all pure C functions (with pure C arguments- no Python API objects), regardless of whether it blocks on IO or CPU for a long time, with Py_BEGIN_ALLOW_THREADS and Py_END_ALLOW_THREADS ?

For example, say you had the following function, where some_pure_c_non_io_func spends a small time on a CPU task. Is there any harm in releasing the GIL here?

static PyObject Foo_bar(Foo *self, PyObject *args, PyObject *kwargs)
{
    int i = 0;

    if (!PyArg_ParseTuple(args, "i", &i)) {
        return NULL;
    }

    Py_BEGIN_ALLOW_THREADS
    some_pure_c_non_io_func(i);
    Py_END_ALLOW_THREADS

    Py_RETURN_NONE;
}
Matthew Moisen
  • 16,701
  • 27
  • 128
  • 231

1 Answers1

2

I'm not totally sure, but from looking at the code it seems there's no harm. Several popular libraries do this for intensive CPU functions, notably Tensorflow and Pillow

Py_BEGIN_ALLOW_THREADS expands to

PyThreadState *_save;
_save = PyEval_SaveThread();

PyEval_SaveThread saves the current thread state in _save (with `_PyThreadState_Swap) and gives up the GIL

Then Py_END_ALLOW_THREADS expands to

PyEval_RestoreThread(_save);

which does the same thing in reverse (takes GIL and makes _save the current thread state).

computergorl
  • 133
  • 1
  • 8
  • 3
    "Several popular libraries do this for intensive CPU functions" isn't really what the question was asking. We know it's worthwhile for CPU intensive functions, but the question is asking about functions that _aren't_ CPU intensive and/or _don't_ do IO? I.e. how quick does a function have to be before you lose time with these calls? – DavidW Nov 06 '20 at 08:01