Commit fd6b4e4d authored by da-woods's avatar da-woods Committed by GitHub

Allow releasing the GIL in nogil functions (GH-4318)

Also adds a check whether we have the GIL before doing so. This
is important because Py_UNBLOCK_THREADS is documented as unsafe
if we don't hold the GIL.

Closes https://github.com/cython/cython/issues/4137
parent 62ff0737
......@@ -2358,7 +2358,7 @@ class CCodeWriter(object):
self.putln("__Pyx_PyGILState_Release(%s);" % variable)
self.putln("#endif")
def put_acquire_gil(self, variable=None):
def put_acquire_gil(self, variable=None, unknown_gil_state=True):
"""
Acquire the GIL. The thread's thread state must have been initialized
by a previous `put_release_gil`
......@@ -2368,15 +2368,26 @@ class CCodeWriter(object):
self.putln("__Pyx_FastGIL_Forget();")
if variable:
self.putln('_save = %s;' % variable)
if unknown_gil_state:
self.putln("if (_save) {")
self.putln("Py_BLOCK_THREADS")
if unknown_gil_state:
self.putln("}")
self.putln("#endif")
def put_release_gil(self, variable=None):
def put_release_gil(self, variable=None, unknown_gil_state=True):
"Release the GIL, corresponds to `put_acquire_gil`."
self.use_fast_gil_utility_code()
self.putln("#ifdef WITH_THREAD")
self.putln("PyThreadState *_save;")
self.putln("_save = NULL;")
if unknown_gil_state:
# we don't *know* that we don't have the GIL (since we may be inside a nogil function,
# and Py_UNBLOCK_THREADS is unsafe without the GIL)
self.putln("if (PyGILState_Check()) {")
self.putln("Py_UNBLOCK_THREADS")
if unknown_gil_state:
self.putln("}")
if variable:
self.putln('%s = _save;' % variable)
self.putln("__Pyx_FastGIL_Remember();")
......
......@@ -8343,9 +8343,12 @@ class GILStatNode(NogilTryFinallyStatNode):
# 'with gil' or 'with nogil' statement
#
# state string 'gil' or 'nogil'
# scope_gil_state_known bool For nogil functions this can be False, since they can also be run with gil
# set to False by GilCheck transform
child_attrs = ["condition"] + NogilTryFinallyStatNode.child_attrs
state_temp = None
scope_gil_state_known = True
def __init__(self, pos, state, body, condition=None):
self.state = state
......@@ -8408,7 +8411,7 @@ class GILStatNode(NogilTryFinallyStatNode):
code.put_ensure_gil(variable=variable)
code.funcstate.gil_owned = True
else:
code.put_release_gil(variable=variable)
code.put_release_gil(variable=variable, unknown_gil_state=not self.scope_gil_state_known)
code.funcstate.gil_owned = False
TryFinallyStatNode.generate_execution_code(self, code)
......@@ -8425,10 +8428,13 @@ class GILExitNode(StatNode):
Used as the 'finally' block in a GILStatNode
state string 'gil' or 'nogil'
# scope_gil_state_known bool For nogil functions this can be False, since they can also be run with gil
# set to False by GilCheck transform
"""
child_attrs = []
state_temp = None
scope_gil_state_known = True
def analyse_expressions(self, env):
return self
......@@ -8442,7 +8448,7 @@ class GILExitNode(StatNode):
if self.state == 'gil':
code.put_release_ensured_gil(variable)
else:
code.put_acquire_gil(variable)
code.put_acquire_gil(variable, unknown_gil_state=not self.scope_gil_state_known)
class EnsureGILNode(GILExitNode):
......
......@@ -3060,6 +3060,7 @@ class GilCheck(VisitorTransform):
self.env_stack.append(node.local_scope)
inner_nogil = node.local_scope.nogil
nogil_declarator_only = self.nogil_declarator_only
if inner_nogil:
self.nogil_declarator_only = True
......@@ -3068,8 +3069,9 @@ class GilCheck(VisitorTransform):
self._visit_scoped_children(node, inner_nogil)
# This cannot be nested, so it doesn't need backup/restore
self.nogil_declarator_only = False
# FuncDefNodes can be nested, because a cpdef function contains a def function
# inside it. Therefore restore to previous state
self.nogil_declarator_only = nogil_declarator_only
self.env_stack.pop()
return node
......@@ -3094,6 +3096,8 @@ class GilCheck(VisitorTransform):
else:
error(node.pos, "Trying to release the GIL while it was "
"previously released.")
if self.nogil_declarator_only:
node.scope_gil_state_known = False
if isinstance(node.finally_clause, Nodes.StatListNode):
# The finally clause of the GILStatNode is a GILExitNode,
......@@ -3144,6 +3148,12 @@ class GilCheck(VisitorTransform):
self.visitchildren(node)
return node
def visit_GILExitNode(self, node):
if self.nogil_declarator_only:
node.scope_gil_state_known = False
self.visitchildren(node)
return node
def visit_Node(self, node):
if self.env_stack and self.nogil and node.nogil_check:
node.nogil_check(self.env_stack[-1])
......
......@@ -758,6 +758,36 @@ static CYTHON_INLINE void * PyThread_tss_get(Py_tss_t *key) {
// PyThread_ReInitTLS() is a no-op
#endif /* TSS (Thread Specific Storage) API */
#if PY_MAJOR_VERSION < 3
#if CYTHON_COMPILING_IN_PYPY
#if PYPY_VERSION_NUM < 0x07030600
#if defined(__cplusplus) && __cplusplus >= 201402L
[[deprecated("`with nogil:` inside a nogil function will not release the GIL in PyPy2 < 7.3.6")]]
#elif defined(__GNUC__) || defined(__clang__)
__attribute__ ((__deprecated__("`with nogil:` inside a nogil function will not release the GIL in PyPy2 < 7.3.6")))
#elif defined(_MSC_VER)
__declspec(deprecated("`with nogil:` inside a nogil function will not release the GIL in PyPy2 < 7.3.6"))
#endif
static CYTHON_INLINE int PyGILState_Check(void) {
// PyGILState_Check is used to decide whether to release the GIL when we don't
// know that we have it. For PyPy2 it isn't possible to check.
// Therefore assume that we don't have the GIL (which causes us not to release it,
// but is "safe")
return 0;
}
#else // PYPY_VERSION_NUM < 0x07030600
// PyPy2 >= 7.3.6 has PyGILState_Check
#endif // PYPY_VERSION_NUM < 0x07030600
#else
// https://stackoverflow.com/a/25666624
static CYTHON_INLINE int PyGILState_Check(void) {
PyThreadState * tstate = _PyThreadState_Current;
return tstate && (tstate == PyGILState_GetThisThreadState());
}
#endif
#endif
#if CYTHON_COMPILING_IN_CPYTHON || defined(_PyDict_NewPresized)
#define __Pyx_PyDict_NewPresized(n) ((n <= 8) ? PyDict_New() : _PyDict_NewPresized(n))
#else
......
......@@ -28,6 +28,46 @@ cdef int g(int x) nogil:
y = x + 42
return y
cdef void release_gil_in_nogil() nogil:
# This should generate valid code with/without the GIL
with nogil:
pass
cpdef void release_gil_in_nogil2() nogil:
# This should generate valid code with/without the GIL
with nogil:
pass
def test_release_gil_in_nogil():
"""
>>> test_release_gil_in_nogil()
"""
with nogil:
release_gil_in_nogil()
with nogil:
release_gil_in_nogil2()
release_gil_in_nogil()
release_gil_in_nogil2()
cdef void get_gil_in_nogil() nogil:
with gil:
pass
cpdef void get_gil_in_nogil2() nogil:
with gil:
pass
def test_get_gil_in_nogil():
"""
>>> test_get_gil_in_nogil()
"""
with nogil:
get_gil_in_nogil()
with nogil:
get_gil_in_nogil2()
get_gil_in_nogil()
get_gil_in_nogil2()
cdef int with_gil_func() except -1 with gil:
raise Exception("error!")
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment