Skip to content

Commit fdf282d

Browse files
bpo-35423: Stop using the "pending calls" machinery for signals. (gh-10972)
This change separates the signal handling trigger in the eval loop from the "pending calls" machinery. There is no semantic change and the difference in performance is insignificant. The change makes both components less confusing. It also eliminates the risk of changes to the pending calls affecting signal handling. This is particularly relevant for some upcoming pending calls changes I have in the works.
1 parent a909460 commit fdf282d

File tree

4 files changed

+94
-25
lines changed

4 files changed

+94
-25
lines changed

Include/internal/pycore_ceval.h

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ struct _ceval_runtime_state {
4545
/* Request for dropping the GIL */
4646
_Py_atomic_int gil_drop_request;
4747
struct _pending_calls pending;
48+
/* Request for checking signals. */
49+
_Py_atomic_int signals_pending;
4850
struct _gil_runtime_state gil;
4951
};
5052

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Separate the signal handling trigger in the eval loop from the "pending
2+
calls" machinery. There is no semantic change and the difference in
3+
performance is insignificant.

Modules/signalmodule.c

+6
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,15 @@ It raises KeyboardInterrupt.");
202202
static int
203203
report_wakeup_write_error(void *data)
204204
{
205+
PyObject *exc, *val, *tb;
205206
int save_errno = errno;
206207
errno = (int) (intptr_t) data;
208+
PyErr_Fetch(&exc, &val, &tb);
207209
PyErr_SetFromErrno(PyExc_OSError);
208210
PySys_WriteStderr("Exception ignored when trying to write to the "
209211
"signal wakeup fd:\n");
210212
PyErr_WriteUnraisable(NULL);
213+
PyErr_Restore(exc, val, tb);
211214
errno = save_errno;
212215
return 0;
213216
}
@@ -216,13 +219,16 @@ report_wakeup_write_error(void *data)
216219
static int
217220
report_wakeup_send_error(void* data)
218221
{
222+
PyObject *exc, *val, *tb;
223+
PyErr_Fetch(&exc, &val, &tb);
219224
/* PyErr_SetExcFromWindowsErr() invokes FormatMessage() which
220225
recognizes the error codes used by both GetLastError() and
221226
WSAGetLastError */
222227
PyErr_SetExcFromWindowsErr(PyExc_OSError, (int) (intptr_t) data);
223228
PySys_WriteStderr("Exception ignored when trying to send to the "
224229
"signal wakeup fd:\n");
225230
PyErr_WriteUnraisable(NULL);
231+
PyErr_Restore(exc, val, tb);
226232
return 0;
227233
}
228234
#endif /* MS_WINDOWS */

Python/ceval.c

+83-25
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ static long dxp[256];
100100
_Py_atomic_store_relaxed( \
101101
&_PyRuntime.ceval.eval_breaker, \
102102
GIL_REQUEST | \
103+
_Py_atomic_load_relaxed(&_PyRuntime.ceval.signals_pending) | \
103104
_Py_atomic_load_relaxed(&_PyRuntime.ceval.pending.calls_to_do) | \
104105
_PyRuntime.ceval.pending.async_exc)
105106

@@ -128,6 +129,18 @@ static long dxp[256];
128129
COMPUTE_EVAL_BREAKER(); \
129130
} while (0)
130131

132+
#define SIGNAL_PENDING_SIGNALS() \
133+
do { \
134+
_Py_atomic_store_relaxed(&_PyRuntime.ceval.signals_pending, 1); \
135+
_Py_atomic_store_relaxed(&_PyRuntime.ceval.eval_breaker, 1); \
136+
} while (0)
137+
138+
#define UNSIGNAL_PENDING_SIGNALS() \
139+
do { \
140+
_Py_atomic_store_relaxed(&_PyRuntime.ceval.signals_pending, 0); \
141+
COMPUTE_EVAL_BREAKER(); \
142+
} while (0)
143+
131144
#define SIGNAL_ASYNC_EXC() \
132145
do { \
133146
_PyRuntime.ceval.pending.async_exc = 1; \
@@ -306,7 +319,7 @@ _PyEval_SignalReceived(void)
306319
/* bpo-30703: Function called when the C signal handler of Python gets a
307320
signal. We cannot queue a callback using Py_AddPendingCall() since
308321
that function is not async-signal-safe. */
309-
SIGNAL_PENDING_CALLS();
322+
SIGNAL_PENDING_SIGNALS();
310323
}
311324

312325
/* This implementation is thread-safe. It allows
@@ -356,44 +369,57 @@ Py_AddPendingCall(int (*func)(void *), void *arg)
356369
return result;
357370
}
358371

359-
int
360-
Py_MakePendingCalls(void)
372+
static int
373+
handle_signals(void)
361374
{
362-
static int busy = 0;
363-
int i;
364-
int r = 0;
365-
366-
assert(PyGILState_Check());
375+
/* Only handle signals on main thread. */
376+
if (_PyRuntime.ceval.pending.main_thread &&
377+
PyThread_get_thread_ident() != _PyRuntime.ceval.pending.main_thread)
378+
{
379+
return 0;
380+
}
367381

368-
if (!_PyRuntime.ceval.pending.lock) {
369-
/* initial allocation of the lock */
370-
_PyRuntime.ceval.pending.lock = PyThread_allocate_lock();
371-
if (_PyRuntime.ceval.pending.lock == NULL)
372-
return -1;
382+
UNSIGNAL_PENDING_SIGNALS();
383+
if (PyErr_CheckSignals() < 0) {
384+
SIGNAL_PENDING_SIGNALS(); /* We're not done yet */
385+
return -1;
373386
}
387+
return 0;
388+
}
389+
390+
static int
391+
make_pending_calls(void)
392+
{
393+
static int busy = 0;
374394

375395
/* only service pending calls on main thread */
376396
if (_PyRuntime.ceval.pending.main_thread &&
377397
PyThread_get_thread_ident() != _PyRuntime.ceval.pending.main_thread)
378398
{
379399
return 0;
380400
}
401+
381402
/* don't perform recursive pending calls */
382-
if (busy)
403+
if (busy) {
383404
return 0;
405+
}
384406
busy = 1;
385407
/* unsignal before starting to call callbacks, so that any callback
386408
added in-between re-signals */
387409
UNSIGNAL_PENDING_CALLS();
410+
int res = 0;
388411

389-
/* Python signal handler doesn't really queue a callback: it only signals
390-
that a signal was received, see _PyEval_SignalReceived(). */
391-
if (PyErr_CheckSignals() < 0) {
392-
goto error;
412+
if (!_PyRuntime.ceval.pending.lock) {
413+
/* initial allocation of the lock */
414+
_PyRuntime.ceval.pending.lock = PyThread_allocate_lock();
415+
if (_PyRuntime.ceval.pending.lock == NULL) {
416+
res = -1;
417+
goto error;
418+
}
393419
}
394420

395421
/* perform a bounded number of calls, in case of recursion */
396-
for (i=0; i<NPENDINGCALLS; i++) {
422+
for (int i=0; i<NPENDINGCALLS; i++) {
397423
int j;
398424
int (*func)(void *);
399425
void *arg = NULL;
@@ -412,19 +438,41 @@ Py_MakePendingCalls(void)
412438
/* having released the lock, perform the callback */
413439
if (func == NULL)
414440
break;
415-
r = func(arg);
416-
if (r) {
441+
res = func(arg);
442+
if (res) {
417443
goto error;
418444
}
419445
}
420446

421447
busy = 0;
422-
return r;
448+
return res;
423449

424450
error:
425451
busy = 0;
426-
SIGNAL_PENDING_CALLS(); /* We're not done yet */
427-
return -1;
452+
SIGNAL_PENDING_CALLS();
453+
return res;
454+
}
455+
456+
/* Py_MakePendingCalls() is a simple wrapper for the sake
457+
of backward-compatibility. */
458+
int
459+
Py_MakePendingCalls(void)
460+
{
461+
assert(PyGILState_Check());
462+
463+
/* Python signal handler doesn't really queue a callback: it only signals
464+
that a signal was received, see _PyEval_SignalReceived(). */
465+
int res = handle_signals();
466+
if (res != 0) {
467+
return res;
468+
}
469+
470+
res = make_pending_calls();
471+
if (res != 0) {
472+
return res;
473+
}
474+
475+
return 0;
428476
}
429477

430478
/* The interpreter's recursion limit */
@@ -957,12 +1005,22 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
9571005
*/
9581006
goto fast_next_opcode;
9591007
}
1008+
1009+
if (_Py_atomic_load_relaxed(
1010+
&_PyRuntime.ceval.signals_pending))
1011+
{
1012+
if (handle_signals() != 0) {
1013+
goto error;
1014+
}
1015+
}
9601016
if (_Py_atomic_load_relaxed(
9611017
&_PyRuntime.ceval.pending.calls_to_do))
9621018
{
963-
if (Py_MakePendingCalls() < 0)
1019+
if (make_pending_calls() != 0) {
9641020
goto error;
1021+
}
9651022
}
1023+
9661024
if (_Py_atomic_load_relaxed(
9671025
&_PyRuntime.ceval.gil_drop_request))
9681026
{

0 commit comments

Comments
 (0)