How is time.sleep implemented in CPython?
I recently got curious as to how the time.sleep
function from Python’s standard library was implemented by CPython - and as it turns out, it’s pretty easy to figure out.
All I had to do was strace
a minimal example Python program that utlizies the function:
1strace python3 -c 'from time import sleep; sleep(0.5); print("Done!");' 2> out.txt
And sure enough, near the very end of strace’s output, we see the following system call:
1pselect6(0, NULL, NULL, NULL, {tv_sec=0, tv_nsec=500000000}, NULL) = 0 (Timeout)
If it weren’t for the very familiar names of the struct timespec
members being listed I wouldn’t have any idea that this syscall was the one responsible for performing the sleep operation.
As I am unfamiliar with the select
and pselect6
syscalls - a couple of questions arose:
- What do
select
andpselect6
have to do with sleeping? - Why use those instead of specialized syscalls -
nanosleep
andclock_nanosleep
?
As always - the answers lie in the manpages. select
and pselect6
are used to monitor groups of file descriptors for readiness of certain IO operations (reading, writing, etc..)
You pass them some file descriptors, and the syscalls block until:
- at least one of them becomes ready for the specified IO operation
- the call is interrupted by a signal handler
- the timeout specified by the
timeout
argument expires
Therefore, a call to pselect6
of the following form:
1pselect6(0, NULL, NULL, NULL, const struct timespec *, NULL);
is equivalent of blocking until the timeout expires and returning, i.e. “sleeping” for the duration of the timeout. That’s all well and good - but it feels “hacky” - the syscalls aren’t being used for their original purpose at all - why not use syscalls designed for sleeping in the first place?
Well, as is often the case - the same question has been asked before.
The answer is not concrete nor exciting: portability. The CHANGE HISTORY sections of select and nanosleep’s
respective POSIX specification pages seem to indicate that select
was first specified in ‘Issue 4 Version 2’ of the standard, released sometime in 1994, and that nanosleep
was standardized in Issue 5, released sometime in 1997 - all according to ‘Single UNIX Specification’
on WikiPedia. According to this answer on the Unix&Linux StackExchange, nanosleep
also seems to have made its way into Linux in version 2.0.30
, released in April 1997. So select
did seem to have a ~3 year headstart on nanosleep
, at least as far as standardization
goes.
That’s all well and good - but it’s 2023. and nanosleep
has been present in the Linux kernel for seemingly 26 years. Why not use it?
Well, I went digging into CPython’s source code to find out. And as it turns out, if we take a peek at the source code for the time.sleep
function at cpython/Modules/timemodule.c
, we see the following bit of code:
1#ifdef HAVE_CLOCK_NANOSLEEP
2 ret = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &timeout_abs, NULL);
3 err = ret;
4#elif defined(HAVE_NANOSLEEP)
5 ret = nanosleep(&timeout_ts, NULL);
6 err = errno;
7#else
8 ret = select(0, (fd_set *)0, (fd_set *)0, (fd_set *)0, &timeout_tv);
9 err = errno;
Huh? So CPython does actually support clock_nanosleep
. In fact it prefers it, and only falls back to select
if the former isn’t supported. So why does my system’s Python interpreter call select
? Well, as it turns out
Arch Linux’s “bleeding edge” package repository is still stuck on Python3 version 3.10.10
. And as the official time.sleep
documentation says:
Changed in version 3.11: On Unix, the
clock_nanosleep()
andnanosleep()
functions are now used if available. On Windows, a waitable timer is now used.
We can confirm this by switching to the 3.10
branch on CPython’s GitHub mirror and checking the time.sleep
definition again:
1#ifndef MS_WINDOWS
2 if (_PyTime_AsTimeval(secs, &timeout, _PyTime_ROUND_CEILING) < 0)
3 return -1;
4
5 Py_BEGIN_ALLOW_THREADS
6 err = select(0, (fd_set *)0, (fd_set *)0, (fd_set *)0, &timeout);
7 Py_END_ALLOW_THREADS
8...
On 3.10, no checks for the availability of clock_nanosleep
or nanosleep
are made, and of course select
is called unconditionally. Python 3.11 was released on Oct. 24, 2022., so it still did take them a fair amount of time to add support
for those newer system calls.