Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-108724: Add PyMutex and _PyParkingLot APIs #109344

Merged
merged 28 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
410840a
gh-108724: Add PyMutex and _PyParkingLot APIs
colesbury Aug 24, 2023
6db39dc
Add news entry
colesbury Sep 12, 2023
e624733
Revert change to Modules/_testcapi/parts.h
colesbury Sep 12, 2023
83e60fb
Update Tools/c-analyzer/cpython/ignored.tsv
colesbury Sep 12, 2023
3161e17
Fix _Py_atomic_store_ptr_release on winarm64
colesbury Sep 12, 2023
3ab7110
Support pthread_stubs.h and require threads for lock tests
colesbury Sep 12, 2023
153a0b3
Clean-up Windows includes and ifdefs
colesbury Sep 12, 2023
f963fd8
Fix Raspbian build
colesbury Sep 12, 2023
6f49ae7
Fix wasm tests.
colesbury Sep 13, 2023
779a401
Fix indentation
colesbury Sep 13, 2023
3200ef5
Rename PyEvent_TimedWait and _PyMutex_LockTimed
colesbury Sep 14, 2023
880a263
more_waiters -> has_more_waiters
colesbury Sep 14, 2023
717b3c9
Remove _PyMutex_State typedef
colesbury Sep 14, 2023
23b91b0
Update docs
colesbury Sep 14, 2023
279c56a
Include cpython/pyatomic.h via pyatomic.h
colesbury Sep 14, 2023
8d8035c
Use compound initializer in test_lock.c
colesbury Sep 14, 2023
417e754
Apply suggestions from code review
colesbury Sep 14, 2023
3ee97b1
Changes from review:
colesbury Sep 14, 2023
95b0d87
Remove thread-local data in parking_lot.c
colesbury Sep 15, 2023
3d1d2e1
Add doc for NUM_BUCKETS
colesbury Sep 15, 2023
e043a8e
Make use of Py_PARK_INTR more explicit in _PySemaphore_PlatformWait
colesbury Sep 15, 2023
6b1d8f9
Statically initialize buckets in parking_lot.c
colesbury Sep 15, 2023
1af0bff
Rename validate_addr to atomic_memcmp
colesbury Sep 15, 2023
f2073d0
Add is_unparking to struct wait_entry in parking_lot.c
colesbury Sep 15, 2023
8f1645b
Use callback in _PyParkingLot_Unpark
colesbury Sep 15, 2023
a0261c1
Simplify _PySemaphore_Wait.
colesbury Sep 15, 2023
6d4565d
check-c-globals: increase limit for parking_lot.c
colesbury Sep 15, 2023
a8ce6be
Changes from review
colesbury Sep 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Include/Python.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
#include "pytypedefs.h"
#include "pybuffer.h"
#include "pystats.h"
#include "pyatomic.h"
#include "object.h"
#include "objimpl.h"
#include "typeslots.h"
Expand Down
9 changes: 3 additions & 6 deletions Include/cpython/pyatomic.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@
// # release
// ...

#ifndef Py_ATOMIC_H
#define Py_ATOMIC_H

#ifndef Py_CPYTHON_ATOMIC_H
# error "this header file must not be included directly"
#endif

// --- _Py_atomic_add --------------------------------------------------------
// Atomically adds `value` to `obj` and returns the previous value
Expand Down Expand Up @@ -501,6 +501,3 @@ static inline void _Py_atomic_fence_release(void);
#else
# error "no available pyatomic implementation for this platform/compiler"
#endif

#endif /* Py_ATOMIC_H */

2 changes: 1 addition & 1 deletion Include/cpython/pyatomic_msc.h
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,7 @@ _Py_atomic_store_ptr_release(void *obj, void *value)
#if defined(_M_X64) || defined(_M_IX86)
*(void * volatile *)obj = value;
#elif defined(_M_ARM64)
__stlr64(obj, (uintptr_t)value);
__stlr64((unsigned __int64 volatile *)obj, (uintptr_t)value);
#else
# error "no implementation of _Py_atomic_store_ptr_release"
#endif
Expand Down
107 changes: 107 additions & 0 deletions Include/internal/pycore_llist.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// A doubly-linked list that can be embedded in a struct.
//
// Usage:
// struct llist_node head = LLIST_INIT(head);
// typedef struct {
// ...
// struct llist_node node;
// ...
// } MyObj;
//
// llist_insert_tail(&head, &obj->node);
// llist_remove(&obj->node);
//
// struct llist_node *node;
// llist_for_each(node, &head) {
// MyObj *obj = llist_data(node, MyObj, node);
// ...
// }
//

#ifndef Py_INTERNAL_LLIST_H
#define Py_INTERNAL_LLIST_H

#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

#ifndef Py_BUILD_CORE
# error "Py_BUILD_CORE must be defined to include this header"
#endif

struct llist_node {
struct llist_node *next;
struct llist_node *prev;
};

// Get the struct containing a node.
#define llist_data(node, type, member) \
(type*)((char*)node - offsetof(type, member))

// Iterate over a list.
#define llist_for_each(node, head) \
for (node = (head)->next; node != (head); node = node->next)

// Iterate over a list, but allow removal of the current node.
#define llist_for_each_safe(node, head) \
for (struct llist_node *_next = (node = (head)->next, node->next); \
node != (head); node = _next, _next = node->next)

#define LLIST_INIT(head) { &head, &head }

static inline void
llist_init(struct llist_node *head)
{
head->next = head;
head->prev = head;
}

// Returns 1 if the list is empty, 0 otherwise.
static inline int
llist_empty(struct llist_node *head)
{
return head->next == head;
}

// Appends to the tail of the list.
static inline void
llist_insert_tail(struct llist_node *head, struct llist_node *node)
{
node->prev = head->prev;
node->next = head;
head->prev->next = node;
head->prev = node;
}

// Remove a node from the list.
static inline void
llist_remove(struct llist_node *node)
{
struct llist_node *prev = node->prev;
struct llist_node *next = node->next;
prev->next = next;
next->prev = prev;
node->prev = NULL;
node->next = NULL;
}

// Append all nodes from head2 onto head1. head2 is left empty.
static inline void
llist_concat(struct llist_node *head1, struct llist_node *head2)
{
if (!llist_empty(head2)) {
head1->prev->next = head2->next;
head2->next->prev = head1->prev;

head1->prev = head2->prev;
head2->prev->next = head1;
llist_init(head2);
}
}

#ifdef __cplusplus
}
#endif
#endif /* !Py_INTERNAL_LLIST_H */
158 changes: 158 additions & 0 deletions Include/internal/pycore_lock.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Lightweight locks and other synchronization mechanisms.
//
// These implementations are based on WebKit's WTF::Lock. See
// https://webkit.org/blog/6161/locking-in-webkit/ for a description of the
// design.
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
#ifndef Py_INTERNAL_LOCK_H
#define Py_INTERNAL_LOCK_H
#ifdef __cplusplus
extern "C" {
#endif

#ifndef Py_BUILD_CORE
# error "this header requires Py_BUILD_CORE define"
#endif

#include "pycore_time.h" // _PyTime_t


// A mutex that occupies one byte. The lock can be zero initialized.
//
// Only the two least significant bits are used. The remaining bits should be
// zero:
// 0b00: unlocked
// 0b01: locked
// 0b10: unlocked and has parked threads
// 0b11: locked and has parked threads
//
// Typical initialization:
// PyMutex m = (PyMutex){0};
//
// Typical usage:
// PyMutex_Lock(&m);
// ...
// PyMutex_Unlock(&m);
typedef struct _PyMutex {
uint8_t v;
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
} PyMutex;

#define _Py_UNLOCKED 0
#define _Py_LOCKED 1
#define _Py_HAS_PARKED 2

// (private) slow path for locking the mutex
PyAPI_FUNC(void) _PyMutex_LockSlow(PyMutex *m);

// (private) slow path for unlocking the mutex
PyAPI_FUNC(void) _PyMutex_UnlockSlow(PyMutex *m);

// Locks the mutex.
//
// If the mutex is currently locked, the calling thread will be parked until
// the mutex is unlocked. If the current thread holds the GIL, then the GIL
// will be released while the thread is parked.
static inline void
PyMutex_Lock(PyMutex *m)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to recommend using "acquire"/"release" instead of "lock"/"unlock", as the former is quite prevalent in this context (and, to my brain, "lock" evokes the object rather than the action). However, there are definitely examples of "lock"/"unlock".

Ultimately, it would be nice if the names were consistent everywhere, to avoid confusing readers. (It would also be nice if the naming were consistent in the industry and academia.) The closer we can get, the better. I'd rather this PR help us converge than diverge, but I haven't taken the time to decide which way the PR goes.😄 (My initial gut reaction was that it diverges.) Feedback on this from a couple of other core devs would be helpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock/unlock is much more common in libraries and programming languages:

  • pthread_mutex_t (POSIX)
  • std::mutex (C++)
  • std::sync::mutex (Rust)
  • java.util.concurrent.locks.Lock (Java)
  • sync.Mutex (Go)

Windows tends to use "wait"/"release" terminology.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I'm fine with lock/unlock (vs. acquire/release) at this point.

Regardless, I don't see this as a blocker for the PR, since it isn't public API (yet--and the name would get more scrutiny during the PEP process).

{
uint8_t expected = _Py_UNLOCKED;
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
if (!_Py_atomic_compare_exchange_uint8(&m->v, &expected, _Py_LOCKED)) {
_PyMutex_LockSlow(m);
}
}

// Unlocks the mutex.
static inline void
PyMutex_Unlock(PyMutex *m)
{
uint8_t expected = _Py_LOCKED;
if (!_Py_atomic_compare_exchange_uint8(&m->v, &expected, _Py_UNLOCKED)) {
_PyMutex_UnlockSlow(m);
}
}

// Checks if the mutex is currently locked.
static inline int
PyMutex_IsLocked(PyMutex *m)
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
{
return (_Py_atomic_load_uint8(&m->v) & _Py_LOCKED) != 0;
}

typedef enum _PyLockFlags {
// Do not detach/release the GIL when waiting on the lock.
_Py_LOCK_DONT_DETACH = 0,

// Detach/release the GIL while waiting on the lock.
_PY_LOCK_DETACH = 1,

// Handle signals if interrupted while waiting on the lock.
_PY_LOCK_HANDLE_SIGNALS = 2,
} _PyLockFlags;

// Lock a mutex with an optional timeout and additional options. See
// _PyLockFlags for details.
extern PyLockStatus
_PyMutex_LockTimed(PyMutex *m, _PyTime_t timeout_ns, _PyLockFlags flags);

// Unlock a mutex, returns 0 if the mutex is not locked (used for improved
// error messages).
extern int _PyMutex_TryUnlock(PyMutex *m);


// PyEvent is a one-time event notification
typedef struct {
uint8_t v;
} PyEvent;

// Set the event and notify any waiting threads.
// Export for '_testinternalcapi' shared extension
PyAPI_FUNC(void) _PyEvent_Notify(PyEvent *evt);

// Wait for the event to be set. If the event is already set, then this returns
// immediately.
PyAPI_FUNC(void) PyEvent_Wait(PyEvent *evt);

// Wait for the event to be set, or until the timeout expires. If the event is
// already set, then this returns immediately. Returns 1 if the event was set,
// and 0 if the timeout expired or thread was interrupted.
PyAPI_FUNC(int) PyEvent_WaitTimed(PyEvent *evt, _PyTime_t timeout_ns);


// _PyRawMutex implements a word-sized mutex that that does not depend on the
// parking lot API, and therefore can be used in the parking lot
// implementation.
//
// The mutex uses a packed representation: the least significant bit is used to
// indicate whether the mutex is locked or not. The remaining bits are either
// zero or a pointer to a `struct raw_mutex_entry` (see lock.c).
typedef struct {
uintptr_t v;
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
} _PyRawMutex;

// Slow paths for lock/unlock
extern void _PyRawMutex_LockSlow(_PyRawMutex *m);
extern void _PyRawMutex_UnlockSlow(_PyRawMutex *m);

static inline void
_PyRawMutex_Lock(_PyRawMutex *m)
{
uintptr_t unlocked = _Py_UNLOCKED;
if (_Py_atomic_compare_exchange_uintptr(&m->v, &unlocked, _Py_LOCKED)) {
return;
}
_PyRawMutex_LockSlow(m);
}

static inline void
_PyRawMutex_Unlock(_PyRawMutex *m)
{
uintptr_t locked = _Py_LOCKED;
if (_Py_atomic_compare_exchange_uintptr(&m->v, &locked, _Py_UNLOCKED)) {
return;
}
_PyRawMutex_UnlockSlow(m);
}

#ifdef __cplusplus
}
#endif
#endif /* !Py_INTERNAL_LOCK_H */
Loading