Skip to content

Commit

Permalink
pythongh-111924: Use PyMutex for Runtime-global Locks. (pythongh-112207)
Browse files Browse the repository at this point in the history
This replaces some usages of PyThread_type_lock with PyMutex, which does not require memory allocation to initialize.

This simplifies some of the runtime initialization and is also one step towards avoiding changing the default raw memory allocator during initialize/finalization, which can be non-thread-safe in some circumstances.
  • Loading branch information
colesbury authored Dec 7, 2023
1 parent db46073 commit cf6110b
Show file tree
Hide file tree
Showing 18 changed files with 97 additions and 241 deletions.
5 changes: 4 additions & 1 deletion Include/internal/pycore_atexit.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#ifndef Py_INTERNAL_ATEXIT_H
#define Py_INTERNAL_ATEXIT_H

#include "pycore_lock.h" // PyMutex

#ifdef __cplusplus
extern "C" {
#endif
Expand All @@ -15,7 +18,7 @@ extern "C" {
typedef void (*atexit_callbackfunc)(void);

struct _atexit_runtime_state {
PyThread_type_lock mutex;
PyMutex mutex;
#define NEXITFUNCS 32
atexit_callbackfunc callbacks[NEXITFUNCS];
int ncallbacks;
Expand Down
3 changes: 1 addition & 2 deletions Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ PyAPI_FUNC(int) _PyEval_MakePendingCalls(PyThreadState *);
#endif

extern void _Py_FinishPendingCalls(PyThreadState *tstate);
extern void _PyEval_InitState(PyInterpreterState *, PyThread_type_lock);
extern void _PyEval_FiniState(struct _ceval_state *ceval);
extern void _PyEval_InitState(PyInterpreterState *);
extern void _PyEval_SignalReceived(PyInterpreterState *interp);

// bitwise flags:
Expand Down
3 changes: 2 additions & 1 deletion Include/internal/pycore_ceval_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif

#include "pycore_lock.h" // PyMutex
#include "pycore_gil.h" // struct _gil_runtime_state


typedef int (*_Py_pending_call_func)(void *);

struct _pending_calls {
int busy;
PyThread_type_lock lock;
PyMutex mutex;
/* Request for running pending calls. */
int32_t calls_to_do;
#define NPENDINGCALLS 32
Expand Down
3 changes: 2 additions & 1 deletion Include/internal/pycore_crossinterp.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif

#include "pycore_lock.h" // PyMutex
#include "pycore_pyerrors.h"


Expand Down Expand Up @@ -128,7 +129,7 @@ struct _xidregitem {
struct _xidregistry {
int global; /* builtin types or heap types */
int initialized;
PyThread_type_lock mutex;
PyMutex mutex;
struct _xidregitem *head;
};

Expand Down
3 changes: 2 additions & 1 deletion Include/internal/pycore_import.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif

#include "pycore_lock.h" // PyMutex
#include "pycore_hashtable.h" // _Py_hashtable_t
#include "pycore_time.h" // _PyTime_t

Expand Down Expand Up @@ -47,7 +48,7 @@ struct _import_runtime_state {
Py_ssize_t last_module_index;
struct {
/* A lock to guard the cache. */
PyThread_type_lock mutex;
PyMutex mutex;
/* The actual cache of (filename, name, PyModuleDef) for modules.
Only legacy (single-phase init) extension modules are added
and only if they support multiple initialization (m_size >- 0)
Expand Down
17 changes: 17 additions & 0 deletions Include/internal/pycore_lock.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ PyMutex_IsLocked(PyMutex *m)
return (_Py_atomic_load_uint8(&m->v) & _Py_LOCKED) != 0;
}

// Re-initializes the mutex after a fork to the unlocked state.
static inline void
_PyMutex_at_fork_reinit(PyMutex *m)
{
memset(m, 0, sizeof(*m));
}

typedef enum _PyLockFlags {
// Do not detach/release the GIL when waiting on the lock.
_Py_LOCK_DONT_DETACH = 0,
Expand All @@ -108,6 +115,16 @@ typedef enum _PyLockFlags {
extern PyLockStatus
_PyMutex_LockTimed(PyMutex *m, _PyTime_t timeout_ns, _PyLockFlags flags);

// Lock a mutex with aditional options. See _PyLockFlags for details.
static inline void
PyMutex_LockFlags(PyMutex *m, _PyLockFlags flags)
{
uint8_t expected = _Py_UNLOCKED;
if (!_Py_atomic_compare_exchange_uint8(&m->v, &expected, _Py_LOCKED)) {
_PyMutex_LockTimed(m, -1, flags);
}
}

// Unlock a mutex, returns 0 if the mutex is not locked (used for improved
// error messages).
extern int _PyMutex_TryUnlock(PyMutex *m);
Expand Down
5 changes: 4 additions & 1 deletion Include/internal/pycore_pymem.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#ifndef Py_INTERNAL_PYMEM_H
#define Py_INTERNAL_PYMEM_H

#include "pycore_lock.h" // PyMutex

#ifdef __cplusplus
extern "C" {
#endif
Expand Down Expand Up @@ -30,7 +33,7 @@ typedef struct {
} debug_alloc_api_t;

struct _pymem_allocators {
PyThread_type_lock mutex;
PyMutex mutex;
struct {
PyMemAllocatorEx raw;
PyMemAllocatorEx mem;
Expand Down
4 changes: 2 additions & 2 deletions Include/internal/pycore_pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,9 @@ PyAPI_FUNC(int) _PyState_AddModule(
extern int _PyOS_InterruptOccurred(PyThreadState *tstate);

#define HEAD_LOCK(runtime) \
PyThread_acquire_lock((runtime)->interpreters.mutex, WAIT_LOCK)
PyMutex_LockFlags(&(runtime)->interpreters.mutex, _Py_LOCK_DONT_DETACH)
#define HEAD_UNLOCK(runtime) \
PyThread_release_lock((runtime)->interpreters.mutex)
PyMutex_Unlock(&(runtime)->interpreters.mutex)

// Get the configuration of the current interpreter.
// The caller must hold the GIL.
Expand Down
4 changes: 2 additions & 2 deletions Include/internal/pycore_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ typedef struct pyruntimestate {
unsigned long _finalizing_id;

struct pyinterpreters {
PyThread_type_lock mutex;
PyMutex mutex;
/* The linked list of interpreters, newest first. */
PyInterpreterState *head;
/* The runtime's initial interpreter, which has a special role
Expand Down Expand Up @@ -234,7 +234,7 @@ typedef struct pyruntimestate {
Py_OpenCodeHookFunction open_code_hook;
void *open_code_userdata;
struct {
PyThread_type_lock mutex;
PyMutex mutex;
_Py_AuditHookEntry *head;
} audit_hooks;

Expand Down
3 changes: 2 additions & 1 deletion Include/internal/pycore_unicodeobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif

#include "pycore_lock.h" // PyMutex
#include "pycore_fileutils.h" // _Py_error_handler
#include "pycore_identifier.h" // _Py_Identifier
#include "pycore_ucnhash.h" // _PyUnicode_Name_CAPI
Expand Down Expand Up @@ -277,7 +278,7 @@ extern PyTypeObject _PyUnicodeASCIIIter_Type;
/* --- Other API ---------------------------------------------------------- */

struct _Py_unicode_runtime_ids {
PyThread_type_lock lock;
PyMutex mutex;
// next_index value must be preserved when Py_Initialize()/Py_Finalize()
// is called multiple times: see _PyUnicode_FromId() implementation.
Py_ssize_t next_index;
Expand Down
61 changes: 16 additions & 45 deletions Objects/obmalloc.c
Original file line number Diff line number Diff line change
Expand Up @@ -329,13 +329,9 @@ int
_PyMem_SetDefaultAllocator(PyMemAllocatorDomain domain,
PyMemAllocatorEx *old_alloc)
{
if (ALLOCATORS_MUTEX == NULL) {
/* The runtime must be initializing. */
return set_default_allocator_unlocked(domain, pydebug, old_alloc);
}
PyThread_acquire_lock(ALLOCATORS_MUTEX, WAIT_LOCK);
PyMutex_Lock(&ALLOCATORS_MUTEX);
int res = set_default_allocator_unlocked(domain, pydebug, old_alloc);
PyThread_release_lock(ALLOCATORS_MUTEX);
PyMutex_Unlock(&ALLOCATORS_MUTEX);
return res;
}

Expand Down Expand Up @@ -467,9 +463,9 @@ set_up_allocators_unlocked(PyMemAllocatorName allocator)
int
_PyMem_SetupAllocators(PyMemAllocatorName allocator)
{
PyThread_acquire_lock(ALLOCATORS_MUTEX, WAIT_LOCK);
PyMutex_Lock(&ALLOCATORS_MUTEX);
int res = set_up_allocators_unlocked(allocator);
PyThread_release_lock(ALLOCATORS_MUTEX);
PyMutex_Unlock(&ALLOCATORS_MUTEX);
return res;
}

Expand Down Expand Up @@ -554,9 +550,9 @@ get_current_allocator_name_unlocked(void)
const char*
_PyMem_GetCurrentAllocatorName(void)
{
PyThread_acquire_lock(ALLOCATORS_MUTEX, WAIT_LOCK);
PyMutex_Lock(&ALLOCATORS_MUTEX);
const char *name = get_current_allocator_name_unlocked();
PyThread_release_lock(ALLOCATORS_MUTEX);
PyMutex_Unlock(&ALLOCATORS_MUTEX);
return name;
}

Expand Down Expand Up @@ -653,14 +649,9 @@ set_up_debug_hooks_unlocked(void)
void
PyMem_SetupDebugHooks(void)
{
if (ALLOCATORS_MUTEX == NULL) {
/* The runtime must not be completely initialized yet. */
set_up_debug_hooks_unlocked();
return;
}
PyThread_acquire_lock(ALLOCATORS_MUTEX, WAIT_LOCK);
PyMutex_Lock(&ALLOCATORS_MUTEX);
set_up_debug_hooks_unlocked();
PyThread_release_lock(ALLOCATORS_MUTEX);
PyMutex_Unlock(&ALLOCATORS_MUTEX);
}

static void
Expand Down Expand Up @@ -696,53 +687,33 @@ set_allocator_unlocked(PyMemAllocatorDomain domain, PyMemAllocatorEx *allocator)
void
PyMem_GetAllocator(PyMemAllocatorDomain domain, PyMemAllocatorEx *allocator)
{
if (ALLOCATORS_MUTEX == NULL) {
/* The runtime must not be completely initialized yet. */
get_allocator_unlocked(domain, allocator);
return;
}
PyThread_acquire_lock(ALLOCATORS_MUTEX, WAIT_LOCK);
PyMutex_Lock(&ALLOCATORS_MUTEX);
get_allocator_unlocked(domain, allocator);
PyThread_release_lock(ALLOCATORS_MUTEX);
PyMutex_Unlock(&ALLOCATORS_MUTEX);
}

void
PyMem_SetAllocator(PyMemAllocatorDomain domain, PyMemAllocatorEx *allocator)
{
if (ALLOCATORS_MUTEX == NULL) {
/* The runtime must not be completely initialized yet. */
set_allocator_unlocked(domain, allocator);
return;
}
PyThread_acquire_lock(ALLOCATORS_MUTEX, WAIT_LOCK);
PyMutex_Lock(&ALLOCATORS_MUTEX);
set_allocator_unlocked(domain, allocator);
PyThread_release_lock(ALLOCATORS_MUTEX);
PyMutex_Unlock(&ALLOCATORS_MUTEX);
}

void
PyObject_GetArenaAllocator(PyObjectArenaAllocator *allocator)
{
if (ALLOCATORS_MUTEX == NULL) {
/* The runtime must not be completely initialized yet. */
*allocator = _PyObject_Arena;
return;
}
PyThread_acquire_lock(ALLOCATORS_MUTEX, WAIT_LOCK);
PyMutex_Lock(&ALLOCATORS_MUTEX);
*allocator = _PyObject_Arena;
PyThread_release_lock(ALLOCATORS_MUTEX);
PyMutex_Unlock(&ALLOCATORS_MUTEX);
}

void
PyObject_SetArenaAllocator(PyObjectArenaAllocator *allocator)
{
if (ALLOCATORS_MUTEX == NULL) {
/* The runtime must not be completely initialized yet. */
_PyObject_Arena = *allocator;
return;
}
PyThread_acquire_lock(ALLOCATORS_MUTEX, WAIT_LOCK);
PyMutex_Lock(&ALLOCATORS_MUTEX);
_PyObject_Arena = *allocator;
PyThread_release_lock(ALLOCATORS_MUTEX);
PyMutex_Unlock(&ALLOCATORS_MUTEX);
}


Expand Down
4 changes: 2 additions & 2 deletions Objects/unicodeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1904,7 +1904,7 @@ _PyUnicode_FromId(_Py_Identifier *id)
if (index < 0) {
struct _Py_unicode_runtime_ids *rt_ids = &interp->runtime->unicode_state.ids;

PyThread_acquire_lock(rt_ids->lock, WAIT_LOCK);
PyMutex_Lock(&rt_ids->mutex);
// Check again to detect concurrent access. Another thread can have
// initialized the index while this thread waited for the lock.
index = _Py_atomic_load_ssize(&id->index);
Expand All @@ -1914,7 +1914,7 @@ _PyUnicode_FromId(_Py_Identifier *id)
rt_ids->next_index++;
_Py_atomic_store_ssize(&id->index, index);
}
PyThread_release_lock(rt_ids->lock);
PyMutex_Unlock(&rt_ids->mutex);
}
assert(index >= 0);

Expand Down
37 changes: 9 additions & 28 deletions Python/ceval_gil.c
Original file line number Diff line number Diff line change
Expand Up @@ -589,9 +589,7 @@ _PyEval_ReInitThreads(PyThreadState *tstate)
take_gil(tstate);

struct _pending_calls *pending = &tstate->interp->ceval.pending;
if (_PyThread_at_fork_reinit(&pending->lock) < 0) {
return _PyStatus_ERR("Can't reinitialize pending calls lock");
}
_PyMutex_at_fork_reinit(&pending->mutex);

/* Destroy all threads except the current one */
_PyThreadState_DeleteExcept(tstate);
Expand Down Expand Up @@ -720,13 +718,10 @@ _PyEval_AddPendingCall(PyInterpreterState *interp,
assert(_Py_IsMainInterpreter(interp));
pending = &_PyRuntime.ceval.pending_mainthread;
}
/* Ensure that _PyEval_InitState() was called
and that _PyEval_FiniState() is not called yet. */
assert(pending->lock != NULL);

PyThread_acquire_lock(pending->lock, WAIT_LOCK);
PyMutex_Lock(&pending->mutex);
int result = _push_pending_call(pending, func, arg, flags);
PyThread_release_lock(pending->lock);
PyMutex_Unlock(&pending->mutex);

/* signal main loop */
SIGNAL_PENDING_CALLS(interp);
Expand Down Expand Up @@ -768,9 +763,9 @@ _make_pending_calls(struct _pending_calls *pending)
int flags = 0;

/* pop one item off the queue while holding the lock */
PyThread_acquire_lock(pending->lock, WAIT_LOCK);
PyMutex_Lock(&pending->mutex);
_pop_pending_call(pending, &func, &arg, &flags);
PyThread_release_lock(pending->lock);
PyMutex_Unlock(&pending->mutex);

/* having released the lock, perform the callback */
if (func == NULL) {
Expand All @@ -795,7 +790,7 @@ make_pending_calls(PyInterpreterState *interp)

/* Only one thread (per interpreter) may run the pending calls
at once. In the same way, we don't do recursive pending calls. */
PyThread_acquire_lock(pending->lock, WAIT_LOCK);
PyMutex_Lock(&pending->mutex);
if (pending->busy) {
/* A pending call was added after another thread was already
handling the pending calls (and had already "unsignaled").
Expand All @@ -807,11 +802,11 @@ make_pending_calls(PyInterpreterState *interp)
care of any remaining pending calls. Until then, though,
all the interpreter's threads will be tripping the eval
breaker every time it's checked. */
PyThread_release_lock(pending->lock);
PyMutex_Unlock(&pending->mutex);
return 0;
}
pending->busy = 1;
PyThread_release_lock(pending->lock);
PyMutex_Unlock(&pending->mutex);

/* unsignal before starting to call callbacks, so that any callback
added in-between re-signals */
Expand Down Expand Up @@ -892,23 +887,9 @@ Py_MakePendingCalls(void)
}

void
_PyEval_InitState(PyInterpreterState *interp, PyThread_type_lock pending_lock)
_PyEval_InitState(PyInterpreterState *interp)
{
_gil_initialize(&interp->_gil);

struct _pending_calls *pending = &interp->ceval.pending;
assert(pending->lock == NULL);
pending->lock = pending_lock;
}

void
_PyEval_FiniState(struct _ceval_state *ceval)
{
struct _pending_calls *pending = &ceval->pending;
if (pending->lock != NULL) {
PyThread_free_lock(pending->lock);
pending->lock = NULL;
}
}


Expand Down
Loading

0 comments on commit cf6110b

Please sign in to comment.