Repository: cython/cython
Pull request: Add GIL-safe wrappers round many blocking C and C++ threading functions
Somewhat fixes #6826.

I still believe we're better off encouraging people to use the standard libraries rather than writing completely agnostic Cython interfaces that handle all cases (including old compilers).

What I've done:
* Added warning comments that no-one will read.
* Created py_safe functions for locking that handle cases where they're called both with and without the GIL. This isn't completely universal (I haven't added them inside types for example) but I think there's enough to use most things correctly.
* Made sure that the C++ call_once py_safe functions reacquire the GIL around the callable. This just isn't possible in C because there's no state.

Problem statement:
[ENH] single-initialization Cython API
I'd like for there to be some API or wrapper around e.g. `std::call_once` or `std::once_flag` in the C++ standard library or equivalent functionality in C23 `threads.h`. You could backfill with default implementations that use e.g. a mutex and an atomic flag for code that doesn't use sufficiently new C or C++ standards.

### Describe the solution you'd like.

I'd like to be able to do something like this:

```cython

@cython.call_once
def initialize_global():
    global cache
    cache = {"hello": "world"}
```

Totally spitballing the API and interface here. In this example the decorator ensures that if multiple threads try to call `initialize_global` simultaneously, only one thread calls the function and all others block.

### Describe alternatives you've considered.

You can do this right now by using e.g. C++ standard library APIs but it would be nice if Cython provided first-class support for this. Also there are deadlock risks that Cython can mitigate somewhat in the general case.

### Additional context

The need for this is more acute on the free-threaded build. AFAICS there isn't any discussion in the free-threading docs about initializing global state in a thread-safe manner. Let me know if I missed something that already exists.

Hints from issue discussion:
My general advice so far to people has been to use the C++ standard library tools where available, and I've been slowly working through and making sure we have wrappings of the main ones. Both C and C++ `call_once` are now available in the main branch.

I've generally been reluctant to start implementing our own versions of this kind of thing. `pymutex` and `critical_section` are exceptions to that because they're really just exposing Python API.

With that said... I wouldn't be opposed to making it easier to use `std::call_once` with a Python object and hopefully handling some of the deadlock stuff there. I've tried to do something like that with `std::stop_callback` in https://github.com/cython/cython/pull/6820/files and I was considering doing something similar with `std::condition_variable(_any)` in the near future. That seems like the most useful first thing to do here.

> AFAICS there isn't any discussion in the free-threading docs about initializing global state in a thread-safe manner. Let me know if I missed something that already exists.

No I don't think there is anything documented. Obviously "do it at module initialization before anything other threads have a chance to start" is generally good advice, but not universally applicable.
> No I don't think there is anything documented. Obviously "do it at module initialization before anything other threads have a chance to start" is generally good advice, but not universally applicable.

I was actually curious about this: how *do* you tell Cython to run code at module init? Put it in module scope?
The main problem with the standard library primitives is they can deadlock with the interpreter. In PyO3 we added a bunch of extension traits for standard library primitives that handle detaching from the runtime before a possibly blocking call:

https://docs.rs/pyo3/latest/pyo3/sync/trait.OnceExt.html#tymethod.call_once_py_attached
> > No I don't think there is anything documented. Obviously "do it at module initialization before anything other threads have a chance to start" is generally good advice, but not universally applicable.
> 
> I was actually curious about this: how _do_ you tell Cython to run code at module init? Put it in module scope?

Yes - just put it in the module scope (like a regular Python module). Obviously you could still spawn a bunch of threads and make your own thread-safety nightmare within that scope, but it should be inaccessible from the outside until it's finished running.
I'll have a look at the PyO3 link and see what I can learn from that. I still think that adding light wrappings to the C++ standard library is probably my preferred approach but I definitely think it should be easy to do the right thing and avoid deadlocks.
The core of it is here:

https://docs.rs/pyo3/latest/src/pyo3/sync.rs.html#641-655.

`SuspendGIL` is an RAII wrapper around `PyGILState_Ensure/Release`.
I've had a go at making GIL-safe wrappers round at least some of the C and C++ wrappings. That includes call_once.

My view is I can probably sneak that into 3.1 because it's "only" library changes, so the risk of it breaking any existing code is pretty minimal (especially since these library wrappings were added very recently).

Commit message: Fixes to cnd

Patch to review:
diff --git a/Cython/Includes/libc/threads.pxd b/Cython/Includes/libc/threads.pxd
index 6c45b7d4c04..17c6dd3ca3f 100644
--- a/Cython/Includes/libc/threads.pxd
+++ b/Cython/Includes/libc/threads.pxd
@@ -1,9 +1,5 @@
 from libc.time cimport timespec
 
-cdef extern from *:
-    # Not part of the C interface, but declaring this here so it isn't nogil
-    ctypedef void (*_once_func_type)() noexcept
-
 cdef extern from *:
     """
     #include <string.h>
@@ -16,12 +12,14 @@ cdef extern from *:
     void once_flag_init "__Pyx_once_flag_init"(once_flag*)
 
 cdef extern from "<threads.h>" nogil:
+    ctypedef void (*_once_func_type)() noexcept nogil
+
     # Threads
     ctypedef struct thrd_t:
         pass
     # Declare as noexcept because you'll regret trying to throw a Python exception from it
     # and nogil because it's a new thread, so you definitely don't have a Python threadstate yet.
-    ctypedef int (*thrd_start_t)(void*) nogil noexcept
+    ctypedef int (*thrd_start_t)(void*) noexcept nogil
 
     int thrd_create(thrd_t*, thrd_start_t, void*)
     int thrd_equal(thrd_t lhs, thrd_t rhs)
@@ -30,6 +28,7 @@ cdef extern from "<threads.h>" nogil:
     void thrd_yield()
     void thrd_exit(int res)
     void thrd_detach(thrd_t thr)
+    # Be very wary of deadlocks if calling thrd_join with the GIL.
     int thrd_join(thrd_t, int *res)
     enum:
         thrd_success
@@ -38,11 +37,13 @@ cdef extern from "<threads.h>" nogil:
         thrd_busy
         thrd_error
 
-    # Mutexes
+    # Mutexes.
+    # We strongly recommend not calling mtx_lock with the GIL held and being
+    # very wary of calling mtx_timed_lock with the GIL (because it will block the GIL until the timeout).
     ctypedef struct mtx_t:
         pass
     int mtx_init(mtx_t* mtx, int type)
-    int mtx_lock(mtx_t* mtx)
+    int mtx_lock(mtx_t* mtx)  # see py_safe_mutex_lock for GIL-safe version
     int mtx_timed_lock(mtx_t* mtx, const timespec* timepoint)
     int mtx_trylock(mtx_t* mtx)
     int mtx_unlock(mtx_t* mtx)
@@ -59,7 +60,11 @@ cdef extern from "<threads.h>" nogil:
         pass
     once_flag ONCE_FLAG_INIT
 
-    # Be careful with the GIL if the Cython function you're passing needs the GIL
+    # We strongly recommend not calling this with the GIL held.
+    # We define some GIL-friendly wrappers for C++ call-once which you can use
+    # in these circumstances.
+    # py_safe_call_once in this file avoids blocking, but the function
+    # must still be nogil.
     void call_once(once_flag* flag, _once_func_type func) noexcept
 
     # condition variables
@@ -68,6 +73,7 @@ cdef extern from "<threads.h>" nogil:
     int cnd_init(cnd_t*)
     int cnd_signal(cnd_t*)
     int cnd_broadcast(cnd_t*)
+    # Be very wary of calling wait/timedwait with the GIL held. Two py_safe_ versions exist.
     int cnd_wait(cnd_t*, mtx_t*)
     int cnd_timedwait(cnd_t*, mtx_t*, const timespec*)
     void cnd_destroy(cnd_t*)
@@ -77,8 +83,152 @@ cdef extern from "<threads.h>" nogil:
         pass
     enum:
         TSS_DTOR_ITERATIONS
-    ctypedef void (*tss_dtor_t)(void*) nogil noexcept
+    ctypedef void (*tss_dtor_t)(void*) noexcept nogil
     int tss_create(tss_t* key, tss_dtor_t destructor)
     void *tss_get(tss_t key)
     int tss_set(tss_t key, void* val)
     void tss_delete(tss_t key)
+
+cdef extern from *:
+    """
+    static int __pyx_libc_threads_has_gil(PyGILState_STATE *gil_state) {
+        #if CYTHON_COMPILING_IN_LIMITED_API
+            /* There is no way to know if we have the GIL. Therefore the only
+            * thing we can safely do is make absolutely sure that we have it.
+            */
+            *gil_state = PyGILState_Ensure();
+            return 1;
+        #else
+            (void)gil_state;
+        #if PY_VERSION_HEX >= 0x030d0000
+            return PyThreadState_GetUnchecked() != NULL;
+        #else
+            return _PyThreadState_UncheckedGet() != NULL;
+        #endif
+        #endif
+    }
+
+    #if CYTHON_COMPILING_IN_LIMITED_API
+    static void __pyx_libc_threads_finish_gil(PyGILState_STATE gil_state) {
+        PyGILState_Release(gil_state);
+    }
+    #else
+    #define __pyx_libc_threads_finish_gil(ignore)
+    #endif
+
+    static int __pyx_py_safe_mtx_lock_slow_spin(mtx_t* mutex) {
+        while (1) {
+            int lock_result;
+            Py_BEGIN_ALLOW_THREADS
+            lock_result = mtx_lock(mutex);
+            if (lock_result != thrd_success)
+                return lock_result;
+            Py_END_ALLOW_THREADS
+            lock_result = mtx_trylock(mutex);
+            if (lock_result != thrd_busy) {
+                return lock_result;
+            }
+        }
+    }
+
+    static int __pyx_py_safe_mtx_lock_quick_spin(mtx_t* mutex) {
+        for (int i=0; i<100; ++i) {
+            Py_BEGIN_ALLOW_THREADS
+            Py_END_ALLOW_THREADS
+            int lock_result = mtx_trylock(mutex);
+            if (lock_result != thrd_busy) {
+                return lock_result;
+            }
+        }
+        return __pyx_py_safe_mtx_lock_slow_spin(mutex);
+    }
+
+    static int __pyx_py_safe_mtx_lock_impl(mtx_t* mutex) {
+        int lock_result = mtx_trylock(mutex);
+        if (lock_result == thrd_busy) {
+            return __pyx_py_safe_mtx_lock_quick_spin(mutex);
+        }
+        return lock_result;
+    }
+
+    static CYTHON_UNUSED int __pyx_py_safe_mtx_lock(mtx_t* mutex) {
+        PyGILState_STATE gil_state;
+        if (!__pyx_libc_threads_has_gil(&gil_state))
+            return mtx_lock(mutex); /* No GIL, no problem */
+        int result = __pyx_py_safe_mtx_lock_impl(mutex);
+        __pyx_libc_threads_finish_gil(gil_state);
+        return result;
+    }
+
+    static int __pyx_py_safe_cnd_wait_impl( cnd_t* cond, mtx_t* mutex) {
+        int result, unlock_result, relock_result;
+        Py_BEGIN_ALLOW_THREADS
+        result = cnd_wait(cond, mutex);
+        unlock_result = mtx_unlock(mutex);
+        Py_END_ALLOW_THREADS
+        // pthreads documentation implies they try to return locked even on error.
+        relock_result = __pyx_py_safe_mtx_lock_impl(mutex);
+        return relock_result != thrd_success ? relock_result :
+            unlock_result != thrd_success ? unlock_result :
+            result;
+    }
+
+    static CYTHON_UNUSED int __pyx_py_safe_cnd_wait( cnd_t* cond, mtx_t* mutex) {
+        PyGILState_STATE gil_state;
+        if (!__pyx_libc_threads_has_gil(&gil_state))
+            return cnd_wait(cond, mutex); /* No GIL, no problem */
+        int result = __pyx_py_safe_cnd_wait_impl(cond, mutex);
+        __pyx_libc_threads_finish_gil(gil_state);
+        return result;
+    }
+
+    static int __pyx_py_safe_cnd_timedwait_impl( cnd_t* cond, mtx_t* mutex, const struct timespec* time_point) {
+        int result, unlock_result, relock_result;
+        Py_BEGIN_ALLOW_THREADS
+        result = cnd_timedwait(cond, mutex, time_point);
+        unlock_result = mtx_unlock(mutex);
+        Py_END_ALLOW_THREADS
+        // pthreads documentation implies they try to return locked even on error.
+        relock_result = __pyx_py_safe_mtx_lock_impl(mutex);
+        return relock_result != thrd_success ? relock_result :
+            unlock_result != thrd_success ? unlock_result :
+            result;
+    }
+
+    static CYTHON_UNUSED int __pyx_py_safe_cnd_timedwait(cnd_t* cond, mtx_t* mutex, const struct timespec* time_point) {
+        PyGILState_STATE gil_state;
+        if (!__pyx_libc_threads_has_gil(&gil_state))
+            return cnd_timedwait(cond, mutex, time_point); /* No GIL, no problem */
+        int result = __pyx_py_safe_cnd_timedwait_impl(cond, mutex, time_point);
+        __pyx_libc_threads_finish_gil(gil_state);
+        return result;
+    }
+
+    static CYTHON_UNUSED void __pyx_libc_threads_py_safe_call_once(once_flag* flag, void (*func)(void)) {
+        PyGILState_STATE gil_state;
+        if (!__pyx_libc_threads_has_gil(&gil_state)) {
+            call_once(flag, func);
+            return;
+        }
+        Py_BEGIN_ALLOW_THREADS
+        call_once(flag, func);
+        Py_END_ALLOW_THREADS
+        __pyx_libc_threads_finish_gil(gil_state);
+    }
+    """
+    # Acquire the lock without deadlocking on the GIL.
+    # At the end, the GIL will be restored to its original state.
+    int py_safe_mtx_lock "__pyx_py_safe_mtx_lock"(mtx_t* mutex) nogil
+
+    # Wait on the condition variable without deadlocking on the GIL.
+    # At the end, the GIL will be restored to its original state.
+    int py_safe_cnd_wait "__pyx_py_safe_cnd_wait"(cnd_t*, mtx_t*) nogil
+    int py_safe_cnd_timedwait "__pyx_py_safe_cnd_timedwait"(cnd_t*, mtx_t*, const timespec*) nogil
+
+    # call_once making sure not to deadlock on the GIL.
+    # The callable must still be nogil though.
+    ctypedef struct py_safe_once_flag "once_flag":
+        pass
+    py_safe_once_flag PY_SAFE_ONCE_FLAG_INIT "ONCE_FLAG_INIT"
+    void py_safe_once_flag_init "__Pyx_once_flag_init"(py_safe_once_flag*)
+    int py_safe_call_once "__pyx_libc_threads_py_safe_call_once"(py_safe_once_flag* flag, _once_func_type func) nogil
diff --git a/Cython/Includes/libc/time.pxd b/Cython/Includes/libc/time.pxd
index fa5e691ed04..301b6962e4a 100644
--- a/Cython/Includes/libc/time.pxd
+++ b/Cython/Includes/libc/time.pxd
@@ -8,6 +8,7 @@ cdef extern from "<time.h>" nogil:
     struct timespec:
         time_t tv_sec
         long tv_nsec
+    enum: TIME_UTC
 
     enum: CLOCKS_PER_SEC
     clock_t clock()             # CPU time
diff --git a/Cython/Includes/libcpp/barrier.pxd b/Cython/Includes/libcpp/barrier.pxd
index 889cb87f5d4..6d73d357635 100644
--- a/Cython/Includes/libcpp/barrier.pxd
+++ b/Cython/Includes/libcpp/barrier.pxd
@@ -1,4 +1,7 @@
 cdef extern from "<barrier>" namespace "std" nogil:
+    # Note on thread safety:
+    # For any of the blocking functions here you should be very
+    # careful to ensure that you are not deadlocked on the GIL.
     cdef cppclass barrier[CompletionFunction = *]:
         cppclass arrival_token:
             pass
diff --git a/Cython/Includes/libcpp/latch.pxd b/Cython/Includes/libcpp/latch.pxd
index 9bdb1ac671d..1d5cba43062 100644
--- a/Cython/Includes/libcpp/latch.pxd
+++ b/Cython/Includes/libcpp/latch.pxd
@@ -1,6 +1,9 @@
 from libcpp cimport bool
 
 cdef extern from "<latch>" namespace "std" nogil:
+    # Note on thread safety:
+    # For any of the blocking functions, you should be very careful
+    # to ensure that you are not deadlocked on the GIL.
     cdef cppclass latch:
         latch(ptrdiff_t expected)
 
diff --git a/Cython/Includes/libcpp/mutex.pxd b/Cython/Includes/libcpp/mutex.pxd
index b0ffb9a3bb0..b208a2e2ae4 100644
--- a/Cython/Includes/libcpp/mutex.pxd
+++ b/Cython/Includes/libcpp/mutex.pxd
@@ -1,10 +1,8 @@
 from libcpp cimport bool
 
-cdef extern from *:
-    # Not part of the C++ interface, but declaring this here so it isn't nogil
-    ctypedef void (*_once_func_type)() except+
-
 cdef extern from "<mutex>" namespace "std" nogil:
+    # For all these mutex classes, we strongly recommend you do not using any
+    # blocking lock function while holding the GIL (try_lock should be fine though).
     cppclass mutex:
         # may not be present, and we know nothing about it
         cppclass native_handle_type:
@@ -29,6 +27,9 @@ cdef extern from "<mutex>" namespace "std" nogil:
 
         native_handle_type native_handle() except+
 
+    # We strongly recommend not mixing recursive_mutex and the GIL at all.
+    # Because "unlock" may not actually unlock it, it's pretty hard to reason about
+    # avoiding deadlocks.
     cppclass recursive_mutex:
         # may not be present, and we know nothing about it
         cppclass native_handle_type:
@@ -40,6 +41,9 @@ cdef extern from "<mutex>" namespace "std" nogil:
 
         native_handle_type native_handle() except+
 
+    # We strongly recommend not mixing recursive_mutex and the GIL at all.
+    # Because "unlock" may not actually unlock it, it's pretty hard to reason about
+    # avoiding deadlocks.
     cppclass timed_recursive_mutex:
         # may not be present, and we know nothing about it
         cppclass native_handle_type:
@@ -67,11 +71,14 @@ cdef extern from "<mutex>" namespace "std" nogil:
 
     # lock_guard is probably unusable without cpp_locals because it
     # can't be default-constructed or moved.
+    # We strongly recommend you do not use this while holding the GIL.
     cppclass lock_guard[T]:
         ctypedef T mutex_type
         lock_guard(mutex_type&) except+
         lock_guard(mutex_type&, adopt_lock_t)
 
+    # We strongly recommend that you do not use any blocking lock function with the GIL.
+    # (try_lock is fine though).
     cppclass unique_lock[T]:
         ctypedef T mutex_type
         unique_lock()
@@ -104,6 +111,7 @@ cdef extern from "<mutex>" namespace "std" nogil:
     # It's also a variadic template type so can take potentially unlimited number of
     # arguments. Cython doesn't support this, so if you want more than 26 arguments,
     # you're on your own.
+    # We strongly recommend that you do not use this while holding the GIL.
     cppclass scoped_lock[A=*, B=*, C=*, D=*, E=*, F=*, G=*, H=*, I=*, J=*, K=*,
                          L=*, M=*, N=*, O=*, P=*, Q=*, R=*, S=*, T=*, U=*, V=*,
                          W=*, X=*, Y=*, Z=*]:
@@ -113,8 +121,238 @@ cdef extern from "<mutex>" namespace "std" nogil:
         pass
 
     bool try_lock(...) except+
+    # We strongly recommend that you do not call "lock" while holding the GIL.
+    # See py_safe_lock.
     void lock(...) except+
 
-    # If we're passed a cdef function, make sure that it's specified without Python exceptions.
-    void call_once(once_flag&, _once_func_type callable) except +
+    # We can't enforce this in the interface, but you need to make sure that Callable
+    # doesn't require the GIL and doesn't throw Python exceptions.
+    # You should also not call this with the GIL held.
+    # We strong recommend using the py_safe_call_once wrappers below if you require the GIL.
     void call_once[Callable](once_flag&, Callable& callable,  ...) except +
+
+
+# Cython-specific wrappers to avoid deadlock.
+# If you're holding the GIL then we strongly recommend you use these.
+cdef extern from *:
+    """
+    #include <utility>
+    #include <exception>
+
+    namespace {
+
+    static int __pyx_libcpp_mutex_has_gil(PyGILState_STATE *gil_state) {
+        #if CYTHON_COMPILING_IN_LIMITED_API
+            /* There is no way to know if we have the GIL. Therefore the only
+            * thing we can safely do is make absolutely sure that we have it.
+            */
+            *gil_state = PyGILState_Ensure();
+            return 1;
+        #else
+            (void)gil_state;
+        #if PY_VERSION_HEX >= 0x030d0000
+            return PyThreadState_GetUnchecked() != NULL;
+        #else
+            return _PyThreadState_UncheckedGet() != NULL;
+        #endif
+        #endif
+    }
+
+    #if CYTHON_COMPILING_IN_LIMITED_API
+    static void __pyx_libcpp_mutex_finish_gil(PyGILState_STATE gil_state) {
+        PyGILState_Release(gil_state);
+    }
+    #else
+    #define __pyx_libcpp_mutex_finish_gil(ignore)
+    #endif
+
+    template <typename F>
+    class __pyx_libcpp_mutex_cleanup_on_exit {
+        F on_exit;
+        bool invoke = true;
+
+    public:
+        explicit __pyx_libcpp_mutex_cleanup_on_exit(F f)
+            : on_exit(f)
+        {}
+        __pyx_libcpp_mutex_cleanup_on_exit(__pyx_libcpp_mutex_cleanup_on_exit &&rhs)
+            : on_exit(std::move(rhs.on_exit))
+            , invoke(std::exchange(rhs.invoke, false))
+        {}
+
+        __pyx_libcpp_mutex_cleanup_on_exit(const __pyx_libcpp_mutex_cleanup_on_exit&) = delete;
+        __pyx_libcpp_mutex_cleanup_on_exit& operator=(const __pyx_libcpp_mutex_cleanup_on_exit&) = delete;
+
+        ~__pyx_libcpp_mutex_cleanup_on_exit() {
+            if (invoke)
+                on_exit();
+        }
+    };
+
+    template<typename F>
+    __pyx_libcpp_mutex_cleanup_on_exit<F> __pyx_make_libcpp_mutex_cleanup_on_exit(F f) {
+        return __pyx_libcpp_mutex_cleanup_on_exit<F>(std::move(f));
+    }
+
+    template <typename Callable, typename ... Args>
+    void __pyx_cpp_py_safe_call_once(std::once_flag& flag, Callable& callable, Args&&... args) {
+        class PyException : public std::exception {
+        public:
+            using std::exception::exception;
+        };
+
+        PyGILState_STATE gil_state;
+        int had_gil_on_call = __pyx_libcpp_mutex_has_gil(&gil_state);
+        PyThreadState *ts = nullptr;
+
+        if (had_gil_on_call)
+            ts = PyEval_SaveThread();
+        auto on_exit = __pyx_make_libcpp_mutex_cleanup_on_exit(
+            [&]() {
+                if (ts)
+                    PyEval_RestoreThread(ts);
+                __pyx_libcpp_mutex_finish_gil(gil_state);
+            });
+
+        try {
+            std::call_once(flag,
+                [&](Args& ...args) {
+                    // Make sure we have the GIL
+                    PyGILState_STATE gil_state;
+                    if (had_gil_on_call) {
+                        PyEval_RestoreThread(ts); ts = 0;
+                    } else {
+                        gil_state = PyGILState_Ensure();
+                    }
+                    auto on_callable_exit = __pyx_make_libcpp_mutex_cleanup_on_exit(
+                        [&]() {
+                            if (had_gil_on_call) {
+                                ts = PyEval_SaveThread();
+                            } else {
+                                PyGILState_Release(gil_state);
+                            }
+                        });
+
+                    std::forward<Callable>(callable)(std::forward<Args>(args)...);
+                    if (PyErr_Occurred()) {
+                        throw PyException();
+                    }
+                },
+                std::forward<Args>(args)...
+            );
+        } catch (const PyException&) {
+            // Do nothing
+        }
+    }
+
+    CYTHON_UNUSED void __pyx_cpp_py_safe_call_object_once(std::once_flag& flag, PyObject *py_callable) {
+        auto callable = [py_callable]() {
+            auto result = PyObject_CallObject(py_callable, nullptr);
+            Py_XDECREF(result);
+        };
+        __pyx_cpp_py_safe_call_once(flag, callable);
+    }
+
+    template <typename LockableT>
+    void __pyx_std_lock_wrapper(LockableT& arg0) {
+        // std::lock only handles 2 or more arguments.
+        // So create a 1 argument version.
+        arg0.lock();
+    }
+
+    template <typename Lockable0T, typename Lockable1T, typename ...Lockables>
+    void __pyx_std_lock_wrapper(Lockable0T& arg0, Lockable1T& arg1, Lockables&... args) {
+        std::lock(arg0, arg1, args...);
+    }
+
+    template <typename LockableT>
+    int __pyx_std_try_lock_wrapper(LockableT& arg0) {
+        // std::try_lock only handles 2 or more arguments.
+        // So create a 1 argument version.
+        if (arg0.try_lock()) {
+            return -1;
+        }
+        return 0;
+    }
+
+    template <typename Lockable0T, typename Lockable1T, typename ...Lockables>
+    int __pyx_std_try_lock_wrapper(Lockable0T& arg0, Lockable1T& arg1, Lockables&... args) {
+        return std::try_lock(arg0, arg1, args...);
+    }
+
+    template <typename... LockTs>
+    void __pyx_py_safe_std_lock_slow_loop(LockTs& ...locks) {
+        while (true) {
+            Py_BEGIN_ALLOW_THREADS
+            __pyx_std_lock_wrapper(locks...);
+            (locks.unlock(), ...);
+            Py_END_ALLOW_THREADS
+            if (__pyx_std_try_lock_wrapper(locks...) == -1) {
+                return; // success
+            }
+        }
+    }
+
+    template <typename... LockTs>
+    void __pyx_py_safe_std_lock_fast_loop(LockTs& ...locks) {
+        for (int i=0; i<100; ++i) {
+            Py_BEGIN_ALLOW_THREADS
+            Py_END_ALLOW_THREADS
+            if (__pyx_std_try_lock_wrapper(locks...) == -1) {
+                return; // success
+            }
+        }
+        __pyx_py_safe_std_lock_slow_loop(locks...);
+    }
+
+    template <typename... LockTs>
+    void __pyx_py_safe_std_lock(LockTs& ...locks) {
+        PyGILState_STATE gil_state;
+        int had_gil_on_call = __pyx_libcpp_mutex_has_gil(&gil_state);
+        auto on_exit = __pyx_make_libcpp_mutex_cleanup_on_exit(
+            [&]() {
+                __pyx_libcpp_mutex_finish_gil(gil_state);
+            });
+        if (!had_gil_on_call) {
+            // Nothing special to do - just lock and quit
+            __pyx_std_lock_wrapper(locks...);
+            return;
+        }
+
+        // It's a real shame there's no try_lock for the GIL, otherwise
+        // we could just defer this whole thing to c++ std::lock.
+        if (__pyx_std_try_lock_wrapper(locks...) == -1) {
+            // success!
+            return;
+        }
+        __pyx_py_safe_std_lock_fast_loop(locks...);
+    }
+
+    template <typename MutexT>
+    std::unique_lock<MutexT> __pyx_py_safe_construct_unique_lock(MutexT& mutex) {
+        std::unique_lock l{mutex, std::defer_lock};
+        __pyx_py_safe_std_lock(l);
+        return l;
+    }
+    } // namespace
+    """
+    # Treat this as a different type from Cython's point of view so that if users use our safe functions
+    # then they can't mix them with the unsafe functions using the same flag.
+    cdef cppclass py_safe_once_flag "std::once_flag":
+        pass
+
+    # Call a Python callable once, ensuring that the GIL is released before locking, and the callable
+    # is called with the GIL held. The callable may be a Python object or C function.
+    # If using a generic callable, the callable can throw either Python
+    # or C++ exceptions. Both are treated like a C++ exception.
+    # The GIL state on exit is the same as on entry.
+    void py_safe_call_object_once "__pyx_cpp_py_safe_call_object_once" (py_safe_once_flag&, object callable) except +* nogil
+    void py_safe_call_once "__pyx_cpp_py_safe_call_once" [Callable](py_safe_once_flag&, Callable callable, ...) except +* nogil
+
+    # Call std::lock on the lockable objects with the GIL held, avoiding deadlock with the GIL.
+    # (Unlike the standard library version, it works with a single argument).
+    # The GIL state on exit is the same as on entry.
+    void py_safe_lock "__pyx_py_safe_std_lock" (...) except+ nogil
+
+    # construct a unique lock with the GIL held, avoiding deadlocks with the GIL
+    unique_lock[MutexT] py_safe_construct_unique_lock "__pyx_py_safe_construct_unique_lock" [MutexT](MutexT& m) except+ nogil
diff --git a/Cython/Includes/libcpp/semaphore.pxd b/Cython/Includes/libcpp/semaphore.pxd
index 30bfd6403ea..3afdc9ba3ec 100644
--- a/Cython/Includes/libcpp/semaphore.pxd
+++ b/Cython/Includes/libcpp/semaphore.pxd
@@ -8,6 +8,12 @@ cdef extern from "<semaphore>" namespace "std" nogil:
     # don't need to use the templates. If you do though, then there's a
     # few tricks - e.g. you can define a class with a cname of "3" for
     # example.
+
+    # Note on thread-safety:
+    # You should use these classes without the GIL. It's very easy
+    # to deadlock with the GIL, and we're assuming that anyone going
+    # low enough level to be using semaphores doesn't want the overhead
+    # of Cython handling GIL safety.
     cdef cppclass counting_semaphore[LeastMaxValue=*]:
         counting_semaphore(ptrdiff_t desired)
 
diff --git a/Cython/Includes/libcpp/shared_mutex.pxd b/Cython/Includes/libcpp/shared_mutex.pxd
index 70b7fee3dad..2e9abadba91 100644
--- a/Cython/Includes/libcpp/shared_mutex.pxd
+++ b/Cython/Includes/libcpp/shared_mutex.pxd
@@ -7,10 +7,12 @@ cdef extern from "<shared_mutex>" namespace "std" nogil:
         cppclass native_handle_type:
             pass
 
+        # We strongly recommend not calling lock with the GIL held (to avoid deadlocks)
         void lock() except+
         bool try_lock()
         void unlock()
 
+        # We strongly recommend not calling lock_shared with the GIL held (to avoid deadlocks)
         void lock_shared() except+
         bool try_lock_shared()
         void unlock_shared()
@@ -23,12 +25,16 @@ cdef extern from "<shared_mutex>" namespace "std" nogil:
         cppclass native_handle_type:
             pass
 
+        # We strongly recommend not calling lock with the GIL held (to avoid deadlocks)
+        # and moderately recommend not calling the timed lock functions with the GIL either.
         void lock() except+
         bool try_lock()
         bool try_lock_for[T](const T& duration) except+
         bool try_lock_until[T](const T& time_point) except+
         void unlock()
 
+        # We strongly recommend not calling lock_shared with the GIL held (to avoid deadlocks)
+        # and moderately recommend not calling the timed lock functions with the GIL either.
         void lock_shared() except+
         bool try_lock_shared()
         bool try_lock_shared_for[T](const T& duration) except+
@@ -37,6 +43,8 @@ cdef extern from "<shared_mutex>" namespace "std" nogil:
 
         native_handle_type native_handle() except+
 
+    # We strongly recommend not attempting to acquire a lock via a shared_lock while holding
+    # the GIL. Use py_safe_construct_shared_lock instead.
     cppclass shared_lock[T]:
         ctypedef T mutex_type
         # This covers both the plain regular constructor, the 3 versions with tags
@@ -46,7 +54,7 @@ cdef extern from "<shared_mutex>" namespace "std" nogil:
         shared_lock(mutex_type&, ...) except+
         #shared_lock(mutex_type&, defer_lock_t)
         #shared_lock(mutex_type&, try_to_lock_t) except+
-        ## this feels like it should be noexcet, but cppreference implies it isn't
+        ## this feels like it should be noexcept, but cppreference implies it isn't
         #shared_lock(mutex_type&, adopt_lock_t) except+
 
         void lock() except+
@@ -64,3 +72,20 @@ cdef extern from "<shared_mutex>" namespace "std" nogil:
         mutex_type* mutex()
         bool owns_lock()
         bool operator bool()
+
+# Just to get our GIL-safe utility code
+cimport libcpp.mutex as _cpp_mutex
+
+cdef extern from *:
+    """
+    namespace {
+        template <typename MutexT>
+        std::shared_lock<MutexT> __pyx_py_safe_construct_shared_lock(MutexT &m) {
+            std::shared_lock<MutexT> l(m, std::defer_lock);
+            __pyx_py_safe_std_lock(l);  // in mutex.pxd
+            return l;
+        }
+    }
+    """
+    # Construct a shared_lock while holding the GIL, without deadlock on the GIL
+    shared_lock[MutexT] py_safe_construct_shared_lock "__pyx_py_safe_construct_shared_lock" [MutexT](MutexT& m) except+ nogil
diff --git a/tests/run/cpp_mutex.pyx b/tests/run/cpp_mutex.pyx
index b796d217a6a..1afbee4858a 100644
--- a/tests/run/cpp_mutex.pyx
+++ b/tests/run/cpp_mutex.pyx
@@ -1,14 +1,26 @@
 # mode: run
 # tag: cpp, cpp17, no-cpp-locals
 
+from __future__ import print_function
+
 from libcpp.mutex cimport (
     mutex, once_flag, unique_lock, call_once,
-    adopt_lock, scoped_lock
+    adopt_lock, scoped_lock,
+    py_safe_call_once, py_safe_call_object_once, py_safe_once_flag,
+    py_safe_construct_unique_lock, py_safe_lock,
 )
 from libcpp.shared_mutex cimport (
-    shared_mutex, shared_lock
+    shared_mutex, shared_lock,
+    py_safe_construct_shared_lock,
 )
 
+from threading import Thread, Barrier
+
+# Note to readers: some of these tests are a bit lazy
+# with the GIL because they know the lock is only being
+# used from one thread. Be very careful with the GIL and
+# C++ locks!
+
 def test_mutex():
     """
     >>> test_mutex()
@@ -33,8 +45,75 @@ def test_unique_lock_more():
     # unlocked automatically when it exits scope
 
 
-cdef void call_me_once() noexcept:
-    print("Listen very carefully, I shall say this only once.")
+def test_py_safe_construct_unique_lock(n_threads):
+    """
+    >>> test_py_safe_construct_unique_lock(4)
+    4
+    """
+
+    cdef mutex m
+    cdef count = 0
+
+    barrier = Barrier(n_threads)
+    def thread_func():
+        nonlocal count
+
+        barrier.wait()
+        # lock is released on function exit
+        lock = py_safe_construct_unique_lock(m)
+        count += 1
+    threads = [ Thread(target=thread_func) for _ in range(n_threads) ]
+    for t in threads:
+        t.start()
+    for t in threads:
+        t.join()
+    print(count)
+
+def test_py_safe_lock(n_threads):
+    """
+    >>> test_py_safe_lock(4)
+    4
+    """
+
+    cdef mutex m1
+    cdef mutex m2
+    cdef count = 0
+
+    barrier = Barrier(n_threads)
+    def thread_func():
+        nonlocal count
+
+        barrier.wait()
+        # lock is released on function exit
+        py_safe_lock(m1, m2)
+        try:
+            count += 1
+        finally:
+            m1.unlock()
+            m2.unlock()
+
+    threads = [ Thread(target=thread_func) for _ in range(n_threads) ]
+    for t in threads:
+        t.start()
+    for t in threads:
+        t.join()
+    print(count)
+
+def test_py_safe_lock_nogil():
+    """
+    >>> test_py_safe_lock_nogil()
+    """
+    cdef mutex m
+    with nogil:
+        py_safe_lock(m)
+        m.unlock()
+
+
+# Note that it is only safe to acquire the GIL because we aren't actually running the
+# tests from multiple threads.
+cdef void call_me_once() noexcept nogil:
+    with gil:
+        print("Listen very carefully, I shall say this only once.")
 
 cdef extern from *:
     """
@@ -56,8 +135,9 @@ def test_once_flag1():
     Listen very carefully, I shall say this only once.
     """
     cdef once_flag flag
-    call_once(flag, call_me_once)
-    call_once(flag, call_me_once)  # This shouldn't do anything.
+    with nogil:
+        call_once(flag, call_me_once)
+        call_once(flag, call_me_once)  # This shouldn't do anything.
 
 def test_once_flag2():
     """
@@ -65,7 +145,8 @@ def test_once_flag2():
     """
     cdef once_flag flag
     try:
-        call_once(flag, call_once_unsuccessfully_cpp)
+        with nogil:
+            call_once(flag, call_once_unsuccessfully_cpp)
         assert False
     except RuntimeError:
         pass
@@ -75,7 +156,88 @@ def test_once_flag3():
     >>> test_once_flag3()
     """
     cdef once_flag flag
-    call_once(flag, call_with_int, 1)
+    with nogil:
+        call_once(flag, call_with_int, 1)
+
+def test_py_safe_once_object():
+    """
+    >>> test_py_safe_once_object()
+    Listen very carefully, I shall say this only once.
+    """
+    cdef py_safe_once_flag flag
+
+    def py_func():
+        print("Listen very carefully, I shall say this only once.")
+
+    n_threads = 4
+    barrier = Barrier(n_threads)
+    def thread_func():
+        barrier.wait()
+        py_safe_call_object_once(flag, py_func)
+    threads = [ Thread(target=thread_func) for _ in range(n_threads) ]
+    for t in threads:
+        t.start()
+    for t in threads:
+        t.join()
+
+def test_py_safe_once_object_fail():
+    """
+    >>> test_py_safe_once_object_fail()
+    Good
+    """
+    cdef py_safe_once_flag flag
+    try:
+        py_safe_call_object_once(flag, None)
+    except TypeError:
+        pass
+    else:
+        assert False
+    py_safe_call_object_once(flag, lambda: print("Good"))
+
+cdef object global_value = 0
+
+cdef void increment_global_value():
+    global global_value
+    global_value += 1
+
+def test_py_safe_once_cdef():
+    """
+    >>> test_py_safe_once_cdef()
+    1
+    """
+    global global_value
+    global_value = 0
+
+    cdef py_safe_once_flag flag
+
+    n_threads = 4
+    barrier = Barrier(n_threads)
+    def thread_func():
+        barrier.wait()
+        py_safe_call_once(flag, increment_global_value)
+    threads = [ Thread(target=thread_func) for _ in range(n_threads) ]
+    for t in threads:
+        t.start()
+    for t in threads:
+        t.join()
+
+    print(global_value)
+
+def test_py_safe_once_cdef_nogil():
+    """
+    >>> test_py_safe_once_cdef_nogil()
+    1
+    """
+    global global_value
+    global_value = 0
+
+    cdef py_safe_once_flag flag
+    with nogil:
+        # function is called with the GIL, but GIL state
+        # on exit is the same as on entry.
+        py_safe_call_once(flag, increment_global_value)
+
+    print(global_value)
 
 
 def test_scoped_lock():
@@ -97,3 +259,25 @@ def test_shared_mutex():
     cdef shared_lock[shared_mutex] l2
     l1 = shared_lock[shared_mutex](m)
     l2 = shared_lock[shared_mutex](m)  # fine - it's shared.
+
+def test_py_safe_construct_shared_lock(n_threads):
+    """
+    >>> test_py_safe_construct_shared_lock(4)
+    Good news - we didn't deadlock
+    """
+
+    cdef shared_mutex m
+
+    barrier1 = Barrier(n_threads)
+    barrier2 = Barrier(n_threads)
+    def thread_func():
+        barrier1.wait()
+        # lock is released on function exit
+        lock = py_safe_construct_shared_lock(m)
+        barrier2.wait()
+    threads = [ Thread(target=thread_func) for _ in range(n_threads) ]
+    for t in threads:
+        t.start()
+    for t in threads:
+        t.join()
+    print("Good news - we didn't deadlock")
diff --git a/tests/run/libc_threads.pyx b/tests/run/libc_threads.pyx
index 2ee88dfaaa6..aaac7cd7334 100644
--- a/tests/run/libc_threads.pyx
+++ b/tests/run/libc_threads.pyx
@@ -1,7 +1,13 @@
 # mode: run
 # tag: c11, no-cpp, no-macos
 
+from __future__ import print_function
+
 from libc cimport threads
+from libc cimport time as ctime
+
+from threading import Thread, Barrier
+import time
 
 def test_mutex():
     """
@@ -11,9 +17,13 @@ def test_mutex():
     threads.mtx_init(&m, threads.mtx_plain)
     threads.mtx_lock(&m)
     threads.mtx_unlock(&m)
+    threads.mtx_destroy(&m)
 
-cdef void call_me_once() noexcept:
-    print("Listen very carefully, I shall say this only once.")
+cdef void call_me_once() noexcept nogil:
+    # with gil is only OK because it's a toy example with no other threads.
+    # Do not copy this code!
+    with gil:
+        print("Listen very carefully, I shall say this only once.")
 
 def test_once():
     """
@@ -25,7 +35,7 @@ def test_once():
     threads.call_once(&flag, &call_me_once)
     threads.call_once(&flag, &call_me_once)
 
-cdef int my_thread_func(void* arg) nogil noexcept:
+cdef int my_thread_func(void* arg) noexcept nogil:
     cdef int x = (<int*>arg)[0]
     t = threads.thrd_current()  # compile test - nothing useful to do with it
     return x
@@ -40,3 +50,155 @@ def test_thread():
     cdef int result = 0
     threads.thrd_join(t, &result)
     assert result==1, result
+
+def test_py_safe_lock(n_threads):
+    """
+    >>> test_py_safe_lock(4)
+    4
+    """
+    cdef threads.mtx_t m
+    threads.mtx_init(&m, threads.mtx_plain)
+
+    barrier = Barrier(n_threads)
+    count = 0
+
+    def thread_func():
+        nonlocal count
+
+        barrier.wait()
+        threads.py_safe_mtx_lock(&m)
+        try:
+            count += 1
+        finally:
+            threads.mtx_unlock(&m)
+
+    all_threads = [
+        Thread(target=thread_func) for _ in range(n_threads)
+    ]
+    for t in all_threads:
+        t.start()
+    for t in all_threads:
+        t.join()
+    print(count)
+    threads.mtx_destroy(&m)
+
+def test_py_safe_lock_nogil():
+    """
+    >>> test_py_safe_lock_nogil()
+    """
+    cdef threads.mtx_t m
+    threads.mtx_init(&m, threads.mtx_plain)
+
+    with nogil:
+        threads.py_safe_mtx_lock(&m)
+        threads.mtx_unlock(&m)
+
+    threads.mtx_destroy(&m)
+
+def test_py_safe_cnd_wait():
+    """
+    >>> test_py_safe_cnd_wait()
+    10 True
+    """
+    cdef threads.cnd_t condition_var
+    threads.cnd_init(&condition_var)
+    cdef threads.mtx_t m
+    threads.mtx_init(&m, threads.mtx_plain)
+
+    cdef object value = 0
+    cdef object done = False
+
+    def wait_on_condition_var():
+        nonlocal value
+        nonlocal done
+
+        threads.py_safe_mtx_lock(&m)
+        try:
+            while True:
+                threads.py_safe_cnd_wait(&condition_var, &m)
+                if value > 5:
+                    done = True
+                return
+        finally:
+            threads.mtx_unlock(&m)
+
+    t1 = Thread(target=wait_on_condition_var)
+    t1.start()
+
+    def set_value():
+        nonlocal value
+        for i in range(10):
+            threads.py_safe_mtx_lock(&m)
+            try:
+                value += 1
+            finally:
+                threads.mtx_unlock(&m)
+            threads.cnd_signal(&condition_var)
+
+    t2 = Thread(target=set_value)
+    t2.start()
+
+    t1.join()
+    t2.join()
+    threads.mtx_destroy(&m)
+    threads.cnd_destroy(&condition_var)
+
+    print(value, done)
+
+def test_py_safe_cnd_timed_wait(fail):
+    """
+    >>> test_py_safe_cnd_timed_wait(False)
+    True
+    >>> test_py_safe_cnd_timed_wait(True)
+    False
+    """
+    cdef threads.cnd_t condition_var
+    threads.cnd_init(&condition_var)
+    cdef threads.mtx_t m
+    threads.mtx_init(&m, threads.mtx_plain)
+
+    condition = False
+    condition_var_succeeded = False
+
+    def wait_on_condition_var():
+        nonlocal condition_var_succeeded
+
+        cdef ctime.timespec ts
+        ctime.timespec_get(&ts, ctime.TIME_UTC);
+        if fail:
+            # don't wait too long to not do anything
+            ts.tv_nsec += 100_000_000  # 100 ms
+            if ts.tv_nsec > 1000_000_000:  # 1s
+                ts.tv_nsec -= 1000_000_000
+                ts.tv_sec += 1
+        else:
+            ts.tv_sec += 10  # make the test robust
+
+        threads.py_safe_mtx_lock(&m)
+        try:
+            while True:
+                result = threads.py_safe_cnd_timedwait(
+                    &condition_var, &m, &ts
+                )
+                if result == threads.thrd_timedout:
+                    return
+                elif result == threads.thrd_success and condition:
+                    condition_var_succeeded = True
+                    return
+        finally:
+            threads.mtx_unlock(&m)
+
+    t = Thread(target=wait_on_condition_var)
+    t.start()
+    threads.py_safe_mtx_lock(&m)
+    try:
+        if not fail:
+            condition = True
+            threads.cnd_broadcast(&condition_var)
+    finally:
+        threads.mtx_unlock(&m);
+
+    t.join()
+    print(condition_var_succeeded)
+    threads.mtx_destroy(&m)
+    threads.cnd_destroy(&condition_var)


Code review comments:
diff_hunk: @@ -77,8 +83,152 @@ cdef extern from "<threads.h>" nogil:
         pass
     enum:
         TSS_DTOR_ITERATIONS
-    ctypedef void (*tss_dtor_t)(void*) nogil noexcept
+    ctypedef void (*tss_dtor_t)(void*) noexcept nogil
     int tss_create(tss_t* key, tss_dtor_t destructor)
     void *tss_get(tss_t key)
     int tss_set(tss_t key, void* val)
     void tss_delete(tss_t key)
+
+cdef extern from *:
+    """
+    static int __pyx_libc_threads_has_gil(PyGILState_STATE *gil_state) {
+        #if CYTHON_COMPILING_IN_LIMITED_API
+            /* There is no way to know if we have the GIL. Therefore the only
+            * thing we can safely do is make absolutely sure that we have it.
+            */
+            *gil_state = PyGILState_Ensure();
+            return 1;
+        #else
+            (void)gil_state;
+        #if PY_VERSION_HEX >= 0x030d0000
+            return PyThreadState_GetUnchecked() != NULL;
+        #else
+            return _PyThreadState_UncheckedGet() != NULL;
+        #endif
+        #endif
+    }
+
+    #if CYTHON_COMPILING_IN_LIMITED_API
+    static void __pyx_libc_threads_finish_gil(PyGILState_STATE gil_state) {
+        PyGILState_Release(gil_state);
+    }
+    #else
+    #define __pyx_libc_threads_finish_gil(ignore)
+    #endif
+
+    static int __pyx_py_safe_mtx_lock_slow_spin(mtx_t* mutex) {
+        while (1) {
+            int lock_result;
+            Py_BEGIN_ALLOW_THREADS
+            lock_result = mtx_lock(mutex);
+            if (lock_result != thrd_success)
+                return lock_result;
+            Py_END_ALLOW_THREADS
path: Cython/Includes/libc/threads.pxd
text: @author:
```suggestion
            lock_result = mtx_unlock(mutex);
            if (lock_result != thrd_success) return lock_result;
            Py_END_ALLOW_THREADS
```