Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9c15485
Factor out struct _pending_call.
ericsnowcurrently Dec 4, 2018
124b3f6
Switch to a linked list.
ericsnowcurrently Dec 4, 2018
c27ef64
Add a TODO.
ericsnowcurrently Dec 4, 2018
25a2e07
Add a NEWS entry.
ericsnowcurrently Dec 11, 2018
c2178a6
Add a note about why we kept NPENDINGCALLS.
ericsnowcurrently Jan 25, 2019
c897687
Preallocate the first 32 pending call entries.
ericsnowcurrently Oct 11, 2023
9bade30
Fix a boundary condition.
ericsnowcurrently Oct 12, 2023
8c776bd
Add a comment.
ericsnowcurrently Oct 12, 2023
bae74fe
Move a test to the right place.
ericsnowcurrently Oct 12, 2023
fcb268c
Distinguish tests for main-only pending calls.
ericsnowcurrently Oct 12, 2023
58b6b4a
Add tests.
ericsnowcurrently Oct 12, 2023
1b223e9
Add _pending_calls.max.
ericsnowcurrently Oct 12, 2023
273e775
Add _pending_calls.maxloop.
ericsnowcurrently Oct 12, 2023
21e1551
NPENDINGCALLS -> NPENDINGCALLSARRAY
ericsnowcurrently Oct 12, 2023
ff77df7
Fix deallocation.
ericsnowcurrently Oct 13, 2023
2ba10ab
Bunp the limit to 1000.
ericsnowcurrently Oct 13, 2023
a5394fa
Drop the limit on the number of pending calls for each interpreter.
ericsnowcurrently Oct 13, 2023
bc284be
Merge branch 'main' into pending-calls-linked-list
ericsnowcurrently Mar 6, 2024
55f9b47
Merge branch 'main' into pending-calls-linked-list
ericsnowcurrently Apr 24, 2024
2f74848
Make sure we make *all* pending calls when finishing.
ericsnowcurrently Apr 25, 2024
5f2404f
Group the pending calls code more closely.
ericsnowcurrently Apr 25, 2024
2357fbb
Do not bother with a preallocated array.
ericsnowcurrently Apr 25, 2024
814cb0c
Use a preallocated array for the initial freelist for the main thread…
ericsnowcurrently Apr 25, 2024
3783a8d
Make _pending_calls_fini() static.
ericsnowcurrently Apr 25, 2024
bbaf872
Initialize the freelist dynamically.
ericsnowcurrently Apr 25, 2024
70c7e62
Merge branch 'main' into pending-calls-linked-list
ericsnowcurrently Apr 26, 2024
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/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ extern void _PyEval_SignalReceived(void);
typedef int _Py_add_pending_call_result;
#define _Py_ADD_PENDING_SUCCESS 0
#define _Py_ADD_PENDING_FULL -1
#define _Py_ADD_PENDING_NO_MEMORY -2

// Export for '_testinternalcapi' shared extension
PyAPI_FUNC(_Py_add_pending_call_result) _PyEval_AddPendingCall(
Expand Down
28 changes: 20 additions & 8 deletions Include/internal/pycore_ceval_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ extern "C" {

typedef int (*_Py_pending_call_func)(void *);

struct _pending_call;

struct _pending_call {
_Py_pending_call_func func;
void *arg;
int flags;
int from_heap;
struct _pending_call *next;
};

#define PENDINGCALLSARRAYSIZE 32

#define MAXPENDINGCALLS PENDINGCALLSARRAYSIZE
// We effectively drop the limit for per-interpreter pending calls.
#define MAXPENDINGCALLS INT32_MAX
/* For interpreter-level pending calls, we want to avoid spending too
much time on pending calls in any one thread, so we apply a limit. */
#if MAXPENDINGCALLS > 100
Expand All @@ -31,7 +34,10 @@ struct _pending_call {
# define MAXPENDINGCALLSLOOP MAXPENDINGCALLS
#endif

#define MAXPENDINGCALLS_MAIN PENDINGCALLSARRAYSIZE
#define NPENDINGCALLSARRAY 32
/* We keep the number small to preserve as much compatibility
as possible with earlier versions. */
#define MAXPENDINGCALLS_MAIN NPENDINGCALLSARRAY
/* For the main thread, we want to make sure all pending calls are
run at once, for the sake of prompt signal handling. This is
unlikely to cause any problems since there should be very few
Expand All @@ -41,7 +47,7 @@ struct _pending_call {
struct _pending_calls {
int busy;
PyMutex mutex;
/* Request for running pending calls. */
/* The number of pending calls. */
int32_t npending;
/* The maximum allowed number of pending calls.
If the queue fills up to this point then _PyEval_AddPendingCall()
Expand All @@ -52,9 +58,10 @@ struct _pending_calls {
A value of 0 means there is no limit (other than the maximum
size of the list of pending calls). */
int32_t maxloop;
struct _pending_call calls[PENDINGCALLSARRAYSIZE];
int first;
int next;
/* The linked list of pending calls. */
struct _pending_call *head;
struct _pending_call *tail;
struct _pending_call *freelist;
};


Expand Down Expand Up @@ -96,6 +103,11 @@ struct _ceval_runtime_state {
// For example, we use a preallocated array
// for the list of pending calls.
struct _pending_calls pending_mainthread;
// Using a preallocated array for the first pending calls gives us
// some extra stability in the case of signals.
// We also use this number as the max and loop max for the main thread.
#define NPENDINGCALLSARRAY 32
struct _pending_call _pending_preallocated[NPENDINGCALLSARRAY];
PyMutex sys_trace_profile_mutex;
};

Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_runtime_init.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ extern PyTypeObject _PyExc_MemoryError;
.autoTSSkey = Py_tss_NEEDS_INIT, \
.parser = _parser_runtime_state_INIT, \
.ceval = { \
.perf = _PyEval_RUNTIME_PERF_INIT, \
.pending_mainthread = { \
.max = MAXPENDINGCALLS_MAIN, \
.maxloop = MAXPENDINGCALLSLOOP_MAIN, \
}, \
.perf = _PyEval_RUNTIME_PERF_INIT, \
}, \
.gilstate = { \
.check_enabled = 1, \
Expand Down
12 changes: 3 additions & 9 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1570,24 +1570,18 @@ def test_max_pending(self):
self.assertEqual(added, maxpending)

with self.subTest('not main-only'):
# Per-interpreter pending calls has the same low limit
# Per-interpreter pending calls do not have a limit
# on how many may be pending at a time.
maxpending = 32

l = []
added = self.pendingcalls_submit(l, 1, main=False)
self.pendingcalls_wait(l, added)
self.assertEqual(added, 1)

l = []
added = self.pendingcalls_submit(l, maxpending, main=False)
self.pendingcalls_wait(l, added)
self.assertEqual(added, maxpending)

l = []
added = self.pendingcalls_submit(l, maxpending+1, main=False)
added = self.pendingcalls_submit(l, 1000, main=False)
self.pendingcalls_wait(l, added)
self.assertEqual(added, maxpending)
self.assertEqual(added, 1000)

class PendingTask(types.SimpleNamespace):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Simply the ceval pending calls list by using a linked list.
174 changes: 133 additions & 41 deletions Python/ceval_gil.c
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,48 @@ signal_active_thread(PyInterpreterState *interp, uintptr_t bit)
threadstate.
*/

static void
_pending_calls_init(struct _pending_calls *pending,
struct _pending_call *preallocated, size_t len)
{
/* We shouldn't need the lock while initializing. */
assert(pending->head == NULL);
assert(pending->tail == NULL);
assert(pending->npending == 0);
assert(!pending->busy);
assert(pending->max > 0);
assert(pending->maxloop >= 0);

assert(pending->freelist == NULL);
if (preallocated) {
assert(len > 0);
for (size_t i = len; i > 0; i--) {
struct _pending_call *call = &preallocated[i-1];
assert(!call->from_heap);
call->next = pending->freelist;
pending->freelist = call;
}
}
}

static void
_pending_calls_fini(struct _pending_calls *pending)
{
PyMutex_Lock(&pending->mutex);
struct _pending_call *head = pending->freelist;
pending->freelist = NULL;
PyMutex_Unlock(&pending->mutex);

/* Deallocate all freelist entries. */
while (head != NULL) {
struct _pending_call *cur = head;
head = cur->next;
if (cur->from_heap) {
PyMem_RawFree(cur);
}
}
}

/* Push one item onto the queue while holding the lock. */
static int
_push_pending_call(struct _pending_calls *pending,
Expand All @@ -684,52 +726,69 @@ _push_pending_call(struct _pending_calls *pending,
}
assert(pending->npending < pending->max);

int i = pending->next;
assert(pending->calls[i].func == NULL);
// Allocate for the pending call.
struct _pending_call *call = pending->freelist;
if (call != NULL) {
pending->freelist = call->next;
}
else {
call = PyMem_RawMalloc(sizeof(struct _pending_call));
if (call == NULL) {
return _Py_ADD_PENDING_NO_MEMORY;
}
call->from_heap = 1;
}

pending->calls[i].func = func;
pending->calls[i].arg = arg;
pending->calls[i].flags = flags;
// Initialize the data.
*call = (struct _pending_call){
.func=func,
.arg=arg,
.flags=flags,
};

assert(pending->npending < PENDINGCALLSARRAYSIZE);
// Add the call to the list.
if (pending->head == NULL) {
pending->head = call;
}
else {
pending->tail->next = call;
}
pending->tail = call;
_Py_atomic_add_int32(&pending->npending, 1);

pending->next = (i + 1) % PENDINGCALLSARRAYSIZE;
assert(pending->next != pending->first
|| pending->npending == pending->max);

return _Py_ADD_PENDING_SUCCESS;
}

static int
_next_pending_call(struct _pending_calls *pending,
int (**func)(void *), void **arg, int *flags)
{
int i = pending->first;
if (pending->npending == 0) {
/* Queue empty */
assert(i == pending->next);
assert(pending->calls[i].func == NULL);
return -1;
}
*func = pending->calls[i].func;
*arg = pending->calls[i].arg;
*flags = pending->calls[i].flags;
return i;
}

/* Pop one item off the queue while holding the lock. */
static void
_pop_pending_call(struct _pending_calls *pending,
int (**func)(void *), void **arg, int *flags)
{
int i = _next_pending_call(pending, func, arg, flags);
if (i >= 0) {
pending->calls[i] = (struct _pending_call){0};
pending->first = (i + 1) % PENDINGCALLSARRAYSIZE;
assert(pending->npending > 0);
_Py_atomic_add_int32(&pending->npending, -1);
struct _pending_call *call = pending->head;
if (call == NULL) {
/* Queue empty */
assert(pending->npending == 0);
assert(pending->tail == NULL);
return;
}
assert(pending->npending > 0);
assert(pending->tail != NULL);

// Remove the next one from the list.
pending->head = call->next;
if (pending->tail == call) {
pending->tail = NULL;
}
_Py_atomic_add_int32(&pending->npending, -1);

// Copy its data.
*func = call->func;
*arg = call->arg;
*flags = call->flags;

// "Deallocate" the list entry.
call->next = pending->freelist;
pending->freelist = call;
}

/* This implementation is thread-safe. It allows
Expand Down Expand Up @@ -807,8 +866,6 @@ _make_pending_calls(struct _pending_calls *pending, int32_t *p_npending)
int res = 0;
int32_t npending = -1;

assert(sizeof(pending->max) <= sizeof(size_t)
&& ((size_t)pending->max) <= Py_ARRAY_LENGTH(pending->calls));
int32_t maxloop = pending->maxloop;
if (maxloop == 0) {
maxloop = pending->max;
Expand All @@ -827,7 +884,7 @@ _make_pending_calls(struct _pending_calls *pending, int32_t *p_npending)
npending = pending->npending;
PyMutex_Unlock(&pending->mutex);

/* Check if there are any more pending calls. */
/* Check if there were any pending calls left. */
if (func == NULL) {
assert(npending == 0);
break;
Expand All @@ -839,6 +896,7 @@ _make_pending_calls(struct _pending_calls *pending, int32_t *p_npending)
PyMem_RawFree(arg);
}
if (res != 0) {
assert(PyErr_Occurred());
res = -1;
goto finally;
}
Expand Down Expand Up @@ -960,11 +1018,37 @@ _Py_FinishPendingCalls(PyThreadState *tstate)
assert(PyGILState_Check());
assert(_PyThreadState_CheckConsistency(tstate));

if (make_pending_calls(tstate) < 0) {
PyObject *exc = _PyErr_GetRaisedException(tstate);
PyErr_BadInternalCall();
_PyErr_ChainExceptions1(exc);
_PyErr_Print(tstate);
struct _pending_calls *pending = &tstate->interp->ceval.pending;
struct _pending_calls *pending_main =
_Py_IsMainThread() && _Py_IsMainInterpreter(tstate->interp)
? &_PyRuntime.ceval.pending_mainthread
: NULL;

/* make_pending_calls() may return early without making all pending
calls, so we keep trying until we're actually done. */
int32_t npending = INT32_MAX;
int32_t npending_prev = -1;
do {
assert(npending_prev < 0 || npending_prev > npending);
npending_prev = npending;

if (make_pending_calls(tstate) < 0) {
PyObject *exc = _PyErr_GetRaisedException(tstate);
PyErr_BadInternalCall();
_PyErr_ChainExceptions1(exc);
_PyErr_Print(tstate);
}

npending = _Py_atomic_load_int32_relaxed(&pending->npending);
if (pending_main != NULL) {
npending += _Py_atomic_load_int32_relaxed(&pending_main->npending);
}
} while (npending > 0);

/* Clean up the lingering pending calls state. */
_pending_calls_fini(pending);
if (pending_main != NULL) {
_pending_calls_fini(pending_main);
}
}

Expand Down Expand Up @@ -1011,6 +1095,14 @@ Py_MakePendingCalls(void)
void
_PyEval_InitState(PyInterpreterState *interp)
{
_pending_calls_init(&interp->ceval.pending, NULL, 0);
if (_Py_IsMainInterpreter(interp)) {
struct _ceval_runtime_state *ceval = &_PyRuntime.ceval;
_pending_calls_init(&ceval->pending_mainthread,
ceval->_pending_preallocated,
Py_ARRAY_LENGTH(ceval->_pending_preallocated));
}

_gil_initialize(&interp->_gil);
}

Expand Down