Skip to content

Commit ac5886b

Browse files
committed
[DRAFT] pythongh-135358 follow-up: Use PyErr_CheckSignalsDetached in the stdlib.
This patch demonstrates how `PyErr_CheckSignalsDetached` (python#135358; see also python#133465 and capi-workgroup/decisions#68) might be used for some (small) efficiency improvements in the stdlib. Almost all existing uses of `PyErr_CheckSignals` in the stdlib appear in loops with this general structure: ```c int res = 0; int async_err = 0; do { Py_BEGIN_ALLOW_THREADS res = system_call(arg1, arg2); Py_END_ALLOW_THREADS } while (res < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals())); if (res < 0) { return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL; } Py_RETURN_NONE; ``` With the addition of `PyErr_CheckSignalsDetached`, this can become ```c int res = 0; int async_err = 0; PyThreadState *tstate = PyEval_SaveThread(); do { res = system_call(arg1, arg2); } while (res < 0 && errno == EINTR && !(async_err = PyErr_CheckSignalsDetached(tstate))); PyEval_RestoreThread(tstate); if (res < 0) { return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL; } Py_RETURN_NONE; ``` There were also a small number of places where ``` Py_BLOCK_THREADS sig = PyErr_CheckSignals(); Py_UNBLOCK_THREADS ``` could become just ``` sig = PyErr_CheckSignalsDetached(tstate); ``` I chose to use explicit `PyErr_{Save,Restore}Thread` throughout this patch, in order to give the thread-state parameter to `PyErr_CheckSignalsDetached` a visible name in the calling code. This is possibly an argument for adding a wrapper macro (`PyErr_CHECK_SIGNALS_DETACHED()`, perhaps) that is part of the `Py_BEGIN_ALLOW_THREADS` family, and is therefore entitled to use the hidden `_state` variable. Or it could be an argument for adding a new construct like ``` Py_WITH_DETACHED_THREAD_STATE (tstate) { do { res = system_call(arg1, arg2); } while (res < 0 && errno == EINTR && !(async_err = PyErr_CheckSignalsDetached(tstate))); } ``` (This could be implemented in plain C by off-label use of a for loop. I’m not sure if that’s a good idea or not. In particular, `break` and `continue` at the top level of a `Py_WITH_DETACHED_THREAD_STATE` block would have surprising effects.) The patch only tackles the most straightforward conversions. Places in the stdlib that could probably benefit from a conversion, but that I did not touch, are: * `signal.sigtimedwait`, `select.select`, `select.poll`, `select.devpoll`, `select.epoll`, `select.kevent`, `time.sleep`, `_multiprocessing.semaphore`, and the ssl and socket modules. All of these places need to do arithmetic on some combination of `struct timespec`, `struct timeval`, and `Py_time_t` in their loops, and the APIs for doing so are not currently safe to use without an attached thread state (in particular, they may set the error indicator). * The ssl and socket modules also do a bunch of other work in the same loops that detach the thread state and I’m not familiar enough with those APIs to know if it’s safe to expand the regions with detached thread state. This is unfortunate as I think these are the loops with the biggest chance of _substantial_ efficiency gains from enlarging those regions. * `time.sleep` is also a horrible maze of `#ifdef`s and I think I see a race bug in the Windows-specific code (filed separately as python#135407). * The tkinter module goes back and forth between holding a Python thread state and holding a _Tcl_ thread state and I didn’t want to mess with that. There are also many places where no conversion is necessary: * `signal.pause`, `signal.raise`, `signal.signal`, `signal.pthread_sigmask`, `signal.pthread_kill`, `os.kill`, `os._getdiskusage`, `PyObject_Print`, `PyObject_Repr`, `PyObject_Str`, `PyErr_SetFromErrnoWithFilenameObjects`, `builtin_input_impl`: These do not contain a loop, so there’s no reason to mess with them. * The `_sre` module, `longobject.c`, `faulthandler.c`, `traceback.c`: These don’t detach the thread state at all, so again there’s no reason to mess with them. As a final note, it would probably be a good idea to make it a documented guarantee that `PyEval_{Save,Restore}Thread` and `PyErr_CheckSignals{,Detached}` preserve the value of `errno` and all the Windows-specific errno analogues. A lot of this code is already assuming that this is the case, and the code that isn’t making that assumption is substantially clunkier for it.
1 parent 6c82e34 commit ac5886b

File tree

10 files changed

+376
-353
lines changed

10 files changed

+376
-353
lines changed

Modules/_io/fileio.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,16 +403,16 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode,
403403

404404
errno = 0;
405405
if (opener == Py_None) {
406+
PyThreadState *tstate = PyEval_SaveThread();
406407
do {
407-
Py_BEGIN_ALLOW_THREADS
408408
#ifdef MS_WINDOWS
409409
self->fd = _wopen(widename, flags, 0666);
410410
#else
411411
self->fd = open(name, flags, 0666);
412412
#endif
413-
Py_END_ALLOW_THREADS
414413
} while (self->fd < 0 && errno == EINTR &&
415-
!(async_err = PyErr_CheckSignals()));
414+
!(async_err = PyErr_CheckSignalsDetached(tstate)));
415+
PyEval_RestoreThread(tstate);
416416

417417
if (async_err)
418418
goto error;

Modules/_io/winconsoleio.c

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ read_console_w(HANDLE handle, DWORD maxlen, DWORD *readlen) {
623623
*readlen = 0;
624624

625625
//DebugBreak();
626-
Py_BEGIN_ALLOW_THREADS
626+
PyThreadState *tstate = PyEval_SaveThread();
627627
DWORD off = 0;
628628
while (off < maxlen) {
629629
DWORD n = (DWORD)-1;
@@ -647,9 +647,7 @@ read_console_w(HANDLE handle, DWORD maxlen, DWORD *readlen) {
647647
if (WaitForSingleObjectEx(hInterruptEvent, 100, FALSE)
648648
== WAIT_OBJECT_0) {
649649
ResetEvent(hInterruptEvent);
650-
Py_BLOCK_THREADS
651-
sig = PyErr_CheckSignals();
652-
Py_UNBLOCK_THREADS
650+
sig = PyErr_CheckSignalsDetached(tstate);
653651
if (sig < 0)
654652
break;
655653
}
@@ -687,8 +685,7 @@ read_console_w(HANDLE handle, DWORD maxlen, DWORD *readlen) {
687685

688686
off += BUFSIZ;
689687
}
690-
691-
Py_END_ALLOW_THREADS
688+
PyEval_RestoreThread(tstate);
692689

693690
if (sig)
694691
goto error;

Modules/_multiprocessing/posixshmem.c

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ _posixshmem_shm_open_impl(PyObject *module, PyObject *path, int flags,
5757
PyErr_SetString(PyExc_ValueError, "embedded null character");
5858
return -1;
5959
}
60+
PyThreadState *tstate = PyEval_SaveThread();
6061
do {
61-
Py_BEGIN_ALLOW_THREADS
6262
fd = shm_open(name, flags, mode);
63-
Py_END_ALLOW_THREADS
64-
} while (fd < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
63+
} while (fd < 0 && errno == EINTR
64+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
65+
PyEval_RestoreThread(tstate);
6566

6667
if (fd < 0) {
6768
if (!async_err)
@@ -102,11 +103,12 @@ _posixshmem_shm_unlink_impl(PyObject *module, PyObject *path)
102103
PyErr_SetString(PyExc_ValueError, "embedded null character");
103104
return NULL;
104105
}
106+
PyThreadState *tstate = PyEval_SaveThread();
105107
do {
106-
Py_BEGIN_ALLOW_THREADS
107108
rv = shm_unlink(name);
108-
Py_END_ALLOW_THREADS
109-
} while (rv < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
109+
} while (rv < 0 && errno == EINTR
110+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
111+
PyEval_RestoreThread(tstate);
110112

111113
if (rv < 0) {
112114
if (!async_err)

Modules/fcntlmodule.c

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,12 @@ fcntl_fcntl_impl(PyObject *module, int fd, int code, PyObject *arg)
7373
}
7474
}
7575

76+
PyThreadState *tstate = PyEval_SaveThread();
7677
do {
77-
Py_BEGIN_ALLOW_THREADS
7878
ret = fcntl(fd, code, (int)int_arg);
79-
Py_END_ALLOW_THREADS
80-
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
79+
} while (ret == -1 && errno == EINTR
80+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
81+
PyEval_RestoreThread(tstate);
8182
if (ret < 0) {
8283
return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
8384
}
@@ -86,23 +87,24 @@ fcntl_fcntl_impl(PyObject *module, int fd, int code, PyObject *arg)
8687
if (PyUnicode_Check(arg) || PyObject_CheckBuffer(arg)) {
8788
Py_buffer view;
8889
#define FCNTL_BUFSZ 1024
89-
/* argument plus NUL byte plus guard to detect a buffer overflow */
90-
char buf[FCNTL_BUFSZ+GUARDSZ];
9190

9291
if (!PyArg_Parse(arg, "s*", &view)) {
9392
return NULL;
9493
}
9594
Py_ssize_t len = view.len;
9695
if (len <= FCNTL_BUFSZ) {
96+
/* argument plus NUL byte plus guard to detect a buffer overflow */
97+
char buf[FCNTL_BUFSZ+GUARDSZ];
9798
memcpy(buf, view.buf, len);
9899
memcpy(buf + len, guard, GUARDSZ);
99100
PyBuffer_Release(&view);
100101

102+
PyThreadState *tstate = PyEval_SaveThread();
101103
do {
102-
Py_BEGIN_ALLOW_THREADS
103104
ret = fcntl(fd, code, buf);
104-
Py_END_ALLOW_THREADS
105-
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
105+
} while (ret == -1 && errno == EINTR
106+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
107+
PyEval_RestoreThread(tstate);
106108
if (ret < 0) {
107109
return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
108110
}
@@ -122,11 +124,12 @@ fcntl_fcntl_impl(PyObject *module, int fd, int code, PyObject *arg)
122124
memcpy(ptr, view.buf, len);
123125
PyBuffer_Release(&view);
124126

127+
PyThreadState *tstate = PyEval_SaveThread();
125128
do {
126-
Py_BEGIN_ALLOW_THREADS
127129
ret = fcntl(fd, code, ptr);
128-
Py_END_ALLOW_THREADS
129-
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
130+
} while (ret == -1 && errno == EINTR
131+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
132+
PyEval_RestoreThread(tstate);
130133
if (ret < 0) {
131134
if (!async_err) {
132135
PyErr_SetFromErrno(PyExc_OSError);
@@ -220,11 +223,12 @@ fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
220223
}
221224
}
222225

226+
PyThreadState *tstate = PyEval_SaveThread();
223227
do {
224-
Py_BEGIN_ALLOW_THREADS
225228
ret = ioctl(fd, code, int_arg);
226-
Py_END_ALLOW_THREADS
227-
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
229+
} while (ret == -1 && errno == EINTR
230+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
231+
PyEval_RestoreThread(tstate);
228232
if (ret < 0) {
229233
return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
230234
}
@@ -244,11 +248,12 @@ fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
244248
memcpy(buf + len, guard, GUARDSZ);
245249
ptr = buf;
246250
}
251+
PyThreadState *tstate = PyEval_SaveThread();
247252
do {
248-
Py_BEGIN_ALLOW_THREADS
249253
ret = ioctl(fd, code, ptr);
250-
Py_END_ALLOW_THREADS
251-
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
254+
} while (ret == -1 && errno == EINTR
255+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
256+
PyEval_RestoreThread(tstate);
252257
if (ret < 0) {
253258
if (!async_err) {
254259
PyErr_SetFromErrno(PyExc_OSError);
@@ -281,11 +286,12 @@ fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
281286
memcpy(buf + len, guard, GUARDSZ);
282287
PyBuffer_Release(&view);
283288

289+
PyThreadState *tstate = PyEval_SaveThread();
284290
do {
285-
Py_BEGIN_ALLOW_THREADS
286291
ret = ioctl(fd, code, buf);
287-
Py_END_ALLOW_THREADS
288-
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
292+
} while (ret == -1 && errno == EINTR
293+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
294+
PyEval_RestoreThread(tstate);
289295
if (ret < 0) {
290296
return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
291297
}
@@ -305,11 +311,12 @@ fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
305311
memcpy(ptr, view.buf, len);
306312
PyBuffer_Release(&view);
307313

314+
PyThreadState *tstate = PyEval_SaveThread();
308315
do {
309-
Py_BEGIN_ALLOW_THREADS
310316
ret = ioctl(fd, code, ptr);
311-
Py_END_ALLOW_THREADS
312-
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
317+
} while (ret == -1 && errno == EINTR
318+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
319+
PyEval_RestoreThread(tstate);
313320
if (ret < 0) {
314321
if (!async_err) {
315322
PyErr_SetFromErrno(PyExc_OSError);
@@ -352,17 +359,18 @@ fcntl_flock_impl(PyObject *module, int fd, int code)
352359
{
353360
int ret;
354361
int async_err = 0;
362+
PyThreadState *tstate;
355363

356364
if (PySys_Audit("fcntl.flock", "ii", fd, code) < 0) {
357365
return NULL;
358366
}
359367

368+
tstate = PyEval_SaveThread();
360369
#ifdef HAVE_FLOCK
361370
do {
362-
Py_BEGIN_ALLOW_THREADS
363371
ret = flock(fd, code);
364-
Py_END_ALLOW_THREADS
365-
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
372+
} while (ret == -1 && errno == EINTR
373+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
366374
#else
367375

368376
#ifndef LOCK_SH
@@ -386,12 +394,12 @@ fcntl_flock_impl(PyObject *module, int fd, int code)
386394
}
387395
l.l_whence = l.l_start = l.l_len = 0;
388396
do {
389-
Py_BEGIN_ALLOW_THREADS
390397
ret = fcntl(fd, (code & LOCK_NB) ? F_SETLK : F_SETLKW, &l);
391-
Py_END_ALLOW_THREADS
392-
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
398+
} while (ret == -1 && errno == EINTR
399+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
393400
}
394401
#endif /* HAVE_FLOCK */
402+
PyEval_RestoreThread(tstate);
395403
if (ret < 0) {
396404
return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
397405
}
@@ -489,11 +497,12 @@ fcntl_lockf_impl(PyObject *module, int fd, int code, PyObject *lenobj,
489497
return NULL;
490498
}
491499
l.l_whence = whence;
500+
PyThreadState *tstate = PyEval_SaveThread();
492501
do {
493-
Py_BEGIN_ALLOW_THREADS
494502
ret = fcntl(fd, (code & LOCK_NB) ? F_SETLK : F_SETLKW, &l);
495-
Py_END_ALLOW_THREADS
496-
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
503+
} while (ret == -1 && errno == EINTR
504+
&& !(async_err = PyErr_CheckSignalsDetached(tstate)));
505+
PyEval_RestoreThread(tstate);
497506
}
498507
if (ret < 0) {
499508
return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;

0 commit comments

Comments
 (0)