Skip to content

Commit c815210

Browse files
authored
gh-109649: Add os.process_cpu_count() function (#109907)
* Refactor os_sched_getaffinity_impl(): move variable definitions to their first assignment. * Fix test_posix.test_sched_getaffinity(): restore the old CPU mask when the test completes! * Doc: Specify that os.cpu_count() counts *logicial* CPUs. * Doc: Specify that os.sched_getaffinity(0) is related to the calling thread.
1 parent 2c23419 commit c815210

File tree

8 files changed

+125
-47
lines changed

8 files changed

+125
-47
lines changed

Doc/library/os.rst

+24-7
Original file line numberDiff line numberDiff line change
@@ -5141,8 +5141,12 @@ operating system.
51415141

51425142
.. function:: sched_getaffinity(pid, /)
51435143

5144-
Return the set of CPUs the process with PID *pid* (or the current process
5145-
if zero) is restricted to.
5144+
Return the set of CPUs the process with PID *pid* is restricted to.
5145+
5146+
If *pid* is zero, return the set of CPUs the calling thread of the current
5147+
process is restricted to.
5148+
5149+
See also the :func:`process_cpu_count` function.
51465150

51475151

51485152
.. _os-path:
@@ -5183,12 +5187,11 @@ Miscellaneous System Information
51835187

51845188
.. function:: cpu_count()
51855189

5186-
Return the number of CPUs in the system. Returns ``None`` if undetermined.
5187-
5188-
This number is not equivalent to the number of CPUs the current process can
5189-
use. The number of usable CPUs can be obtained with
5190-
``len(os.sched_getaffinity(0))``
5190+
Return the number of logical CPUs in the **system**. Returns ``None`` if
5191+
undetermined.
51915192

5193+
The :func:`process_cpu_count` function can be used to get the number of
5194+
logical CPUs usable by the calling thread of the **current process**.
51925195

51935196
.. versionadded:: 3.4
51945197

@@ -5202,6 +5205,20 @@ Miscellaneous System Information
52025205
.. availability:: Unix.
52035206

52045207

5208+
.. function:: process_cpu_count()
5209+
5210+
Get the number of logical CPUs usable by the calling thread of the **current
5211+
process**. Returns ``None`` if undetermined. It can be less than
5212+
:func:`cpu_count` depending on the CPU affinity.
5213+
5214+
The :func:`cpu_count` function can be used to get the number of logical CPUs
5215+
in the **system**.
5216+
5217+
See also the :func:`sched_getaffinity` functions.
5218+
5219+
.. versionadded:: 3.13
5220+
5221+
52055222
.. function:: sysconf(name, /)
52065223

52075224
Return integer-valued system configuration values. If the configuration value

Doc/whatsnew/3.13.rst

+7
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ opcode
163163
documented or exposed through ``dis``, and were not intended to be
164164
used externally.
165165

166+
os
167+
--
168+
169+
* Add :func:`os.process_cpu_count` function to get the number of logical CPUs
170+
usable by the calling thread of the current process.
171+
(Contributed by Victor Stinner in :gh:`109649`.)
172+
166173
pathlib
167174
-------
168175

Lib/os.py

+14
Original file line numberDiff line numberDiff line change
@@ -1136,3 +1136,17 @@ def add_dll_directory(path):
11361136
cookie,
11371137
nt._remove_dll_directory
11381138
)
1139+
1140+
1141+
if _exists('sched_getaffinity'):
1142+
def process_cpu_count():
1143+
"""
1144+
Get the number of CPUs of the current process.
1145+
1146+
Return the number of logical CPUs usable by the calling thread of the
1147+
current process. Return None if indeterminable.
1148+
"""
1149+
return len(sched_getaffinity(0))
1150+
else:
1151+
# Just an alias to cpu_count() (same docstring)
1152+
process_cpu_count = cpu_count

Lib/test/test_os.py

+32-4
Original file line numberDiff line numberDiff line change
@@ -3996,14 +3996,42 @@ def test_oserror_filename(self):
39963996
self.fail(f"No exception thrown by {func}")
39973997

39983998
class CPUCountTests(unittest.TestCase):
3999+
def check_cpu_count(self, cpus):
4000+
if cpus is None:
4001+
self.skipTest("Could not determine the number of CPUs")
4002+
4003+
self.assertIsInstance(cpus, int)
4004+
self.assertGreater(cpus, 0)
4005+
39994006
def test_cpu_count(self):
40004007
cpus = os.cpu_count()
4001-
if cpus is not None:
4002-
self.assertIsInstance(cpus, int)
4003-
self.assertGreater(cpus, 0)
4004-
else:
4008+
self.check_cpu_count(cpus)
4009+
4010+
def test_process_cpu_count(self):
4011+
cpus = os.process_cpu_count()
4012+
self.assertLessEqual(cpus, os.cpu_count())
4013+
self.check_cpu_count(cpus)
4014+
4015+
@unittest.skipUnless(hasattr(os, 'sched_setaffinity'),
4016+
"don't have sched affinity support")
4017+
def test_process_cpu_count_affinity(self):
4018+
ncpu = os.cpu_count()
4019+
if ncpu is None:
40054020
self.skipTest("Could not determine the number of CPUs")
40064021

4022+
# Disable one CPU
4023+
mask = os.sched_getaffinity(0)
4024+
if len(mask) <= 1:
4025+
self.skipTest(f"sched_getaffinity() returns less than "
4026+
f"2 CPUs: {sorted(mask)}")
4027+
self.addCleanup(os.sched_setaffinity, 0, list(mask))
4028+
mask.pop()
4029+
os.sched_setaffinity(0, mask)
4030+
4031+
# test process_cpu_count()
4032+
affinity = os.process_cpu_count()
4033+
self.assertEqual(affinity, ncpu - 1)
4034+
40074035

40084036
# FD inheritance check is only useful for systems with process support.
40094037
@support.requires_subprocess()

Lib/test/test_posix.py

+1
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,7 @@ def test_sched_getaffinity(self):
12051205
@requires_sched_affinity
12061206
def test_sched_setaffinity(self):
12071207
mask = posix.sched_getaffinity(0)
1208+
self.addCleanup(posix.sched_setaffinity, 0, list(mask))
12081209
if len(mask) > 1:
12091210
# Empty masks are forbidden
12101211
mask.pop()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :func:`os.process_cpu_count` function to get the number of logical CPUs
2+
usable by the calling thread of the current process. Patch by Victor Stinner.

Modules/clinic/posixmodule.c.h

+3-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/posixmodule.c

+42-31
Original file line numberDiff line numberDiff line change
@@ -8133,39 +8133,45 @@ static PyObject *
81338133
os_sched_getaffinity_impl(PyObject *module, pid_t pid)
81348134
/*[clinic end generated code: output=f726f2c193c17a4f input=983ce7cb4a565980]*/
81358135
{
8136-
int cpu, ncpus, count;
8136+
int ncpus = NCPUS_START;
81378137
size_t setsize;
8138-
cpu_set_t *mask = NULL;
8139-
PyObject *res = NULL;
8138+
cpu_set_t *mask;
81408139

8141-
ncpus = NCPUS_START;
81428140
while (1) {
81438141
setsize = CPU_ALLOC_SIZE(ncpus);
81448142
mask = CPU_ALLOC(ncpus);
8145-
if (mask == NULL)
8143+
if (mask == NULL) {
81468144
return PyErr_NoMemory();
8147-
if (sched_getaffinity(pid, setsize, mask) == 0)
8145+
}
8146+
if (sched_getaffinity(pid, setsize, mask) == 0) {
81488147
break;
8148+
}
81498149
CPU_FREE(mask);
8150-
if (errno != EINVAL)
8150+
if (errno != EINVAL) {
81518151
return posix_error();
8152+
}
81528153
if (ncpus > INT_MAX / 2) {
8153-
PyErr_SetString(PyExc_OverflowError, "could not allocate "
8154-
"a large enough CPU set");
8154+
PyErr_SetString(PyExc_OverflowError,
8155+
"could not allocate a large enough CPU set");
81558156
return NULL;
81568157
}
8157-
ncpus = ncpus * 2;
8158+
ncpus *= 2;
81588159
}
81598160

8160-
res = PySet_New(NULL);
8161-
if (res == NULL)
8161+
PyObject *res = PySet_New(NULL);
8162+
if (res == NULL) {
81628163
goto error;
8163-
for (cpu = 0, count = CPU_COUNT_S(setsize, mask); count; cpu++) {
8164+
}
8165+
8166+
int cpu = 0;
8167+
int count = CPU_COUNT_S(setsize, mask);
8168+
for (; count; cpu++) {
81648169
if (CPU_ISSET_S(cpu, setsize, mask)) {
81658170
PyObject *cpu_num = PyLong_FromLong(cpu);
81668171
--count;
8167-
if (cpu_num == NULL)
8172+
if (cpu_num == NULL) {
81688173
goto error;
8174+
}
81698175
if (PySet_Add(res, cpu_num)) {
81708176
Py_DECREF(cpu_num);
81718177
goto error;
@@ -8177,12 +8183,12 @@ os_sched_getaffinity_impl(PyObject *module, pid_t pid)
81778183
return res;
81788184

81798185
error:
8180-
if (mask)
8186+
if (mask) {
81818187
CPU_FREE(mask);
8188+
}
81828189
Py_XDECREF(res);
81838190
return NULL;
81848191
}
8185-
81868192
#endif /* HAVE_SCHED_SETAFFINITY */
81878193

81888194
#endif /* HAVE_SCHED_H */
@@ -14333,44 +14339,49 @@ os_get_terminal_size_impl(PyObject *module, int fd)
1433314339
/*[clinic input]
1433414340
os.cpu_count
1433514341
14336-
Return the number of CPUs in the system; return None if indeterminable.
14342+
Return the number of logical CPUs in the system.
1433714343
14338-
This number is not equivalent to the number of CPUs the current process can
14339-
use. The number of usable CPUs can be obtained with
14340-
``len(os.sched_getaffinity(0))``
14344+
Return None if indeterminable.
1434114345
[clinic start generated code]*/
1434214346

1434314347
static PyObject *
1434414348
os_cpu_count_impl(PyObject *module)
14345-
/*[clinic end generated code: output=5fc29463c3936a9c input=e7c8f4ba6dbbadd3]*/
14349+
/*[clinic end generated code: output=5fc29463c3936a9c input=ba2f6f8980a0e2eb]*/
1434614350
{
14347-
int ncpu = 0;
14351+
int ncpu;
1434814352
#ifdef MS_WINDOWS
14349-
#ifdef MS_WINDOWS_DESKTOP
14353+
# ifdef MS_WINDOWS_DESKTOP
1435014354
ncpu = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
14351-
#endif
14355+
# else
14356+
ncpu = 0;
14357+
# endif
14358+
1435214359
#elif defined(__hpux)
1435314360
ncpu = mpctl(MPC_GETNUMSPUS, NULL, NULL);
14361+
1435414362
#elif defined(HAVE_SYSCONF) && defined(_SC_NPROCESSORS_ONLN)
1435514363
ncpu = sysconf(_SC_NPROCESSORS_ONLN);
14364+
1435614365
#elif defined(__VXWORKS__)
1435714366
ncpu = _Py_popcount32(vxCpuEnabledGet());
14367+
1435814368
#elif defined(__DragonFly__) || \
1435914369
defined(__OpenBSD__) || \
1436014370
defined(__FreeBSD__) || \
1436114371
defined(__NetBSD__) || \
1436214372
defined(__APPLE__)
14363-
int mib[2];
14373+
ncpu = 0;
1436414374
size_t len = sizeof(ncpu);
14365-
mib[0] = CTL_HW;
14366-
mib[1] = HW_NCPU;
14367-
if (sysctl(mib, 2, &ncpu, &len, NULL, 0) != 0)
14375+
int mib[2] = {CTL_HW, HW_NCPU};
14376+
if (sysctl(mib, 2, &ncpu, &len, NULL, 0) != 0) {
1436814377
ncpu = 0;
14378+
}
1436914379
#endif
14370-
if (ncpu >= 1)
14371-
return PyLong_FromLong(ncpu);
14372-
else
14380+
14381+
if (ncpu < 1) {
1437314382
Py_RETURN_NONE;
14383+
}
14384+
return PyLong_FromLong(ncpu);
1437414385
}
1437514386

1437614387

0 commit comments

Comments
 (0)