Skip to content

Commit 781cc68

Browse files
authored
pythongh-137109: refactor warning about threads when forking (python#141438)
* pythongh-137109: refactor warning about threads when forking This splits the OS API specific functionality to get the number of threads out from the fallback Python method and warning raising code itself. This way the OS APIs can be queried before we've run `os.register_at_fork(after_in_parent=...)` registered functions which themselves may (re)start threads that would otherwise be detected. This is best effort. If the OS APIs are either unavailable or fail, the warning generating code still falls back to looking at the Python threading state after the CPython interpreter world has been restarted and the after_in_parent calls have been made. The common case for most Linux and macOS environments should work today. This also lines up with the existing TODO refactoring, we may choose to expose this API to get the number of OS threads in the `os` module in the future. * NEWS entry * avoid "function-prototype" compiler warning?
1 parent 26b7df2 commit 781cc68

File tree

2 files changed

+65
-43
lines changed

2 files changed

+65
-43
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The :mod:`os.fork` and related forking APIs will no longer warn in the
2+
common case where Linux or macOS platform APIs return the number of threads
3+
in a process and find the answer to be 1 even when a
4+
:func:`os.register_at_fork` ``after_in_parent=`` callback (re)starts a
5+
thread.

Modules/posixmodule.c

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8431,53 +8431,19 @@ os_register_at_fork_impl(PyObject *module, PyObject *before,
84318431
// running in the process. Best effort, silent if unable to count threads.
84328432
// Constraint: Quick. Never overcounts. Never leaves an error set.
84338433
//
8434-
// This should only be called from the parent process after
8434+
// This MUST only be called from the parent process after
84358435
// PyOS_AfterFork_Parent().
84368436
static int
8437-
warn_about_fork_with_threads(const char* name)
8437+
warn_about_fork_with_threads(
8438+
const char* name, // Name of the API to use in the warning message.
8439+
const Py_ssize_t num_os_threads // Only trusted when >= 1.
8440+
)
84388441
{
84398442
// It's not safe to issue the warning while the world is stopped, because
84408443
// other threads might be holding locks that we need, which would deadlock.
84418444
assert(!_PyRuntime.stoptheworld.world_stopped);
84428445

8443-
// TODO: Consider making an `os` module API to return the current number
8444-
// of threads in the process. That'd presumably use this platform code but
8445-
// raise an error rather than using the inaccurate fallback.
8446-
Py_ssize_t num_python_threads = 0;
8447-
#if defined(__APPLE__) && defined(HAVE_GETPID)
8448-
mach_port_t macos_self = mach_task_self();
8449-
mach_port_t macos_task;
8450-
if (task_for_pid(macos_self, getpid(), &macos_task) == KERN_SUCCESS) {
8451-
thread_array_t macos_threads;
8452-
mach_msg_type_number_t macos_n_threads;
8453-
if (task_threads(macos_task, &macos_threads,
8454-
&macos_n_threads) == KERN_SUCCESS) {
8455-
num_python_threads = macos_n_threads;
8456-
}
8457-
}
8458-
#elif defined(__linux__)
8459-
// Linux /proc/self/stat 20th field is the number of threads.
8460-
FILE* proc_stat = fopen("/proc/self/stat", "r");
8461-
if (proc_stat) {
8462-
size_t n;
8463-
// Size chosen arbitrarily. ~60% more bytes than a 20th column index
8464-
// observed on the author's workstation.
8465-
char stat_line[160];
8466-
n = fread(&stat_line, 1, 159, proc_stat);
8467-
stat_line[n] = '\0';
8468-
fclose(proc_stat);
8469-
8470-
char *saveptr = NULL;
8471-
char *field = strtok_r(stat_line, " ", &saveptr);
8472-
unsigned int idx;
8473-
for (idx = 19; idx && field; --idx) {
8474-
field = strtok_r(NULL, " ", &saveptr);
8475-
}
8476-
if (idx == 0 && field) { // found the 20th field
8477-
num_python_threads = atoi(field); // 0 on error
8478-
}
8479-
}
8480-
#endif
8446+
Py_ssize_t num_python_threads = num_os_threads;
84818447
if (num_python_threads <= 0) {
84828448
// Fall back to just the number our threading module knows about.
84838449
// An incomplete view of the world, but better than nothing.
@@ -8530,6 +8496,51 @@ warn_about_fork_with_threads(const char* name)
85308496
}
85318497
return 0;
85328498
}
8499+
8500+
// If this returns <= 0, we were unable to successfully use any OS APIs.
8501+
// Returns a positive number of threads otherwise.
8502+
static Py_ssize_t get_number_of_os_threads(void)
8503+
{
8504+
// TODO: Consider making an `os` module API to return the current number
8505+
// of threads in the process. That'd presumably use this platform code but
8506+
// raise an error rather than using the inaccurate fallback.
8507+
Py_ssize_t num_python_threads = 0;
8508+
#if defined(__APPLE__) && defined(HAVE_GETPID)
8509+
mach_port_t macos_self = mach_task_self();
8510+
mach_port_t macos_task;
8511+
if (task_for_pid(macos_self, getpid(), &macos_task) == KERN_SUCCESS) {
8512+
thread_array_t macos_threads;
8513+
mach_msg_type_number_t macos_n_threads;
8514+
if (task_threads(macos_task, &macos_threads,
8515+
&macos_n_threads) == KERN_SUCCESS) {
8516+
num_python_threads = macos_n_threads;
8517+
}
8518+
}
8519+
#elif defined(__linux__)
8520+
// Linux /proc/self/stat 20th field is the number of threads.
8521+
FILE* proc_stat = fopen("/proc/self/stat", "r");
8522+
if (proc_stat) {
8523+
size_t n;
8524+
// Size chosen arbitrarily. ~60% more bytes than a 20th column index
8525+
// observed on the author's workstation.
8526+
char stat_line[160];
8527+
n = fread(&stat_line, 1, 159, proc_stat);
8528+
stat_line[n] = '\0';
8529+
fclose(proc_stat);
8530+
8531+
char *saveptr = NULL;
8532+
char *field = strtok_r(stat_line, " ", &saveptr);
8533+
unsigned int idx;
8534+
for (idx = 19; idx && field; --idx) {
8535+
field = strtok_r(NULL, " ", &saveptr);
8536+
}
8537+
if (idx == 0 && field) { // found the 20th field
8538+
num_python_threads = atoi(field); // 0 on error
8539+
}
8540+
}
8541+
#endif
8542+
return num_python_threads;
8543+
}
85338544
#endif // HAVE_FORK1 || HAVE_FORKPTY || HAVE_FORK
85348545

85358546
#ifdef HAVE_FORK1
@@ -8564,10 +8575,12 @@ os_fork1_impl(PyObject *module)
85648575
/* child: this clobbers and resets the import lock. */
85658576
PyOS_AfterFork_Child();
85668577
} else {
8578+
// Called before AfterFork_Parent in case those hooks start threads.
8579+
Py_ssize_t num_os_threads = get_number_of_os_threads();
85678580
/* parent: release the import lock. */
85688581
PyOS_AfterFork_Parent();
85698582
// After PyOS_AfterFork_Parent() starts the world to avoid deadlock.
8570-
if (warn_about_fork_with_threads("fork1") < 0) {
8583+
if (warn_about_fork_with_threads("fork1", num_os_threads) < 0) {
85718584
return NULL;
85728585
}
85738586
}
@@ -8615,10 +8628,12 @@ os_fork_impl(PyObject *module)
86158628
/* child: this clobbers and resets the import lock. */
86168629
PyOS_AfterFork_Child();
86178630
} else {
8631+
// Called before AfterFork_Parent in case those hooks start threads.
8632+
Py_ssize_t num_os_threads = get_number_of_os_threads();
86188633
/* parent: release the import lock. */
86198634
PyOS_AfterFork_Parent();
86208635
// After PyOS_AfterFork_Parent() starts the world to avoid deadlock.
8621-
if (warn_about_fork_with_threads("fork") < 0)
8636+
if (warn_about_fork_with_threads("fork", num_os_threads) < 0)
86228637
return NULL;
86238638
}
86248639
if (pid == -1) {
@@ -9476,6 +9491,8 @@ os_forkpty_impl(PyObject *module)
94769491
/* child: this clobbers and resets the import lock. */
94779492
PyOS_AfterFork_Child();
94789493
} else {
9494+
// Called before AfterFork_Parent in case those hooks start threads.
9495+
Py_ssize_t num_os_threads = get_number_of_os_threads();
94799496
/* parent: release the import lock. */
94809497
PyOS_AfterFork_Parent();
94819498
/* set O_CLOEXEC on master_fd */
@@ -9485,7 +9502,7 @@ os_forkpty_impl(PyObject *module)
94859502
}
94869503

94879504
// After PyOS_AfterFork_Parent() starts the world to avoid deadlock.
9488-
if (warn_about_fork_with_threads("forkpty") < 0)
9505+
if (warn_about_fork_with_threads("forkpty", num_os_threads) < 0)
94899506
return NULL;
94909507
}
94919508
if (pid == -1) {

0 commit comments

Comments
 (0)