Skip to content

bpo-21302: time.sleep() uses waitable timer on Windows #28483

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions Doc/library/time.rst
Original file line number Diff line number Diff line change
Expand Up @@ -351,22 +351,35 @@ Functions

Suspend execution of the calling thread for the given number of seconds.
The argument may be a floating point number to indicate a more precise sleep
time. The actual suspension time may be less than that requested because any
caught signal will terminate the :func:`sleep` following execution of that
signal's catching routine. Also, the suspension time may be longer than
requested by an arbitrary amount because of the scheduling of other activity
in the system.
time.

If the sleep is interrupted by a signal and no exception is raised by the
signal handler, the sleep is restarted with a recomputed timeout.

The suspension time may be longer than requested by an arbitrary amount,
because of the scheduling of other activity in the system.

On Windows, if *secs* is zero, the thread relinquishes the remainder of its
time slice to any other thread that is ready to run. If there are no other
threads ready to run, the function returns immediately, and the thread
continues execution.

Implementation:

* On Unix, ``clock_nanosleep()`` is used if available (resolution: 1 ns),
or ``select()`` is used otherwise (resolution: 1 us).
* On Windows, a waitable timer is used (resolution: 100 ns). If *secs* is
zero, ``Sleep(0)`` is used.

.. versionchanged:: 3.11
On Unix, the ``clock_nanosleep()`` function is now used if available.
On Windows, a waitable timer is now used.

.. versionchanged:: 3.5
The function now sleeps at least *secs* even if the sleep is interrupted
by a signal, except if the signal handler raises an exception (see
:pep:`475` for the rationale).

.. versionchanged:: 3.11
In Unix operating systems, the ``clock_nanosleep()`` function is now
used, if available: it allows to sleep for an interval specified with
nanosecond precision.


.. index::
single: % (percent); datetime format
Expand Down
11 changes: 8 additions & 3 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,14 @@ sqlite3
time
----

* In Unix operating systems, :func:`time.sleep` now uses the
``clock_nanosleep()`` function, if available, which allows to sleep for an
interval specified with nanosecond precision.
* On Unix, :func:`time.sleep` now uses the ``clock_nanosleep()`` function, if
available, which has a resolution of 1 ns (10^-6 sec), rather than using
``select()`` which has a resolution of 1 us (10^-9 sec).
(Contributed by Livius and Victor Stinner in :issue:`21302`.)

* On Windows, :func:`time.sleep` now uses a waitable timer which has a
resolution of 100 ns (10^-7 sec). Previously, it had a solution of 1 ms
(10^-3 sec).
(Contributed by Livius and Victor Stinner in :issue:`21302`.)

unicodedata
Expand Down
6 changes: 6 additions & 0 deletions Include/cpython/pytime.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ PyAPI_FUNC(_PyTime_t) _PyTime_AsMicroseconds(_PyTime_t t,
/* Convert timestamp to a number of nanoseconds (10^-9 seconds). */
PyAPI_FUNC(_PyTime_t) _PyTime_AsNanoseconds(_PyTime_t t);

#ifdef MS_WINDOWS
// Convert timestamp to a number of 100 nanoseconds (10^-7 seconds).
PyAPI_FUNC(_PyTime_t) _PyTime_As100Nanoseconds(_PyTime_t t,
_PyTime_round_t round);
#endif

/* Convert timestamp to a number of nanoseconds (10^-9 seconds) as a Python int
object. */
PyAPI_FUNC(PyObject *) _PyTime_AsNanosecondsObject(_PyTime_t t);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
On Windows, :func:`time.sleep` now uses a waitable timer which has a resolution
of 100 ns (10^-7 sec). Previously, it had a solution of 1 ms (10^-3 sec).
Patch by Livius and Victor Stinner.
153 changes: 110 additions & 43 deletions Modules/timemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,9 @@ time_sleep(PyObject *self, PyObject *obj)
"sleep length must be non-negative");
return NULL;
}
if (pysleep(secs) != 0)
if (pysleep(secs) != 0) {
return NULL;
}
Py_RETURN_NONE;
}

Expand Down Expand Up @@ -2044,47 +2045,42 @@ PyInit_time(void)
return PyModuleDef_Init(&timemodule);
}

/* Implement pysleep() for various platforms.
When interrupted (or when another error occurs), return -1 and
set an exception; else return 0. */

// time.sleep() implementation.
// On error, raise an exception and return -1.
// On success, return 0.
static int
pysleep(_PyTime_t secs)
{
_PyTime_t deadline, monotonic;
assert(secs >= 0);

#ifndef MS_WINDOWS
#ifdef HAVE_CLOCK_NANOSLEEP
struct timespec timeout_abs;
#else
struct timeval timeout;
#endif
_PyTime_t deadline, monotonic;
int err = 0;
int ret = 0;
#else
_PyTime_t millisecs;
unsigned long ul_millis;
DWORD rc;
HANDLE hInterruptEvent;
#endif

if (get_monotonic(&monotonic) < 0) {
return -1;
}
deadline = monotonic + secs;
#if defined(HAVE_CLOCK_NANOSLEEP) && !defined(MS_WINDOWS)
#ifdef HAVE_CLOCK_NANOSLEEP
if (_PyTime_AsTimespec(deadline, &timeout_abs) < 0) {
return -1;
}
#endif

do {
#ifndef MS_WINDOWS
#ifndef HAVE_CLOCK_NANOSLEEP
if (_PyTime_AsTimeval(secs, &timeout, _PyTime_ROUND_CEILING) < 0) {
return -1;
}
#endif

int ret;
#ifdef HAVE_CLOCK_NANOSLEEP
Py_BEGIN_ALLOW_THREADS
ret = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &timeout_abs, NULL);
Expand All @@ -2106,35 +2102,6 @@ pysleep(_PyTime_t secs)
PyErr_SetFromErrno(PyExc_OSError);
return -1;
}
#else
millisecs = _PyTime_AsMilliseconds(secs, _PyTime_ROUND_CEILING);
if (millisecs > (double)ULONG_MAX) {
PyErr_SetString(PyExc_OverflowError,
"sleep length is too large");
return -1;
}

/* Allow sleep(0) to maintain win32 semantics, and as decreed
* by Guido, only the main thread can be interrupted.
*/
ul_millis = (unsigned long)millisecs;
if (ul_millis == 0 || !_PyOS_IsMainThread()) {
Py_BEGIN_ALLOW_THREADS
Sleep(ul_millis);
Py_END_ALLOW_THREADS
break;
}

hInterruptEvent = _PyOS_SigintEvent();
ResetEvent(hInterruptEvent);

Py_BEGIN_ALLOW_THREADS
rc = WaitForSingleObjectEx(hInterruptEvent, ul_millis, FALSE);
Py_END_ALLOW_THREADS

if (rc != WAIT_OBJECT_0)
break;
#endif

/* sleep was interrupted by SIGINT */
if (PyErr_CheckSignals()) {
Expand All @@ -2154,4 +2121,104 @@ pysleep(_PyTime_t secs)
} while (1);

return 0;
#else // MS_WINDOWS
_PyTime_t timeout = _PyTime_As100Nanoseconds(secs, _PyTime_ROUND_CEILING);

// Maintain Windows Sleep() semantics for time.sleep(0)
if (timeout == 0) {
Py_BEGIN_ALLOW_THREADS
// A value of zero causes the thread to relinquish the remainder of its
// time slice to any other thread that is ready to run. If there are no
// other threads ready to run, the function returns immediately, and
// the thread continues execution.
Sleep(0);
Py_END_ALLOW_THREADS
return 0;
}

LARGE_INTEGER relative_timeout;
// No need to check for integer overflow, both types are signed
assert(sizeof(relative_timeout) == sizeof(timeout));
// SetWaitableTimer(): a negative due time indicates relative time
relative_timeout.QuadPart = -timeout;

HANDLE timer = CreateWaitableTimerW(NULL, FALSE, NULL);
if (timer == NULL) {
PyErr_SetFromWindowsErr(0);
return -1;
}

if (!SetWaitableTimer(timer, &relative_timeout,
// period: the timer is signaled once
0,
// no completion routine
NULL, NULL,
// Don't restore a system in suspended power
// conservation mode when the timer is signaled.
FALSE))
{
PyErr_SetFromWindowsErr(0);
goto error;
}

// Only the main thread can be interrupted by SIGINT.
// Signal handlers are only executed in the main thread.
if (_PyOS_IsMainThread()) {
HANDLE sigint_event = _PyOS_SigintEvent();

while (1) {
// Check for pending SIGINT signal before resetting the event
if (PyErr_CheckSignals()) {
goto error;
}
ResetEvent(sigint_event);

HANDLE events[] = {timer, sigint_event};
DWORD rc;

Py_BEGIN_ALLOW_THREADS
rc = WaitForMultipleObjects(Py_ARRAY_LENGTH(events), events,
// bWaitAll
FALSE,
// No wait timeout
INFINITE);
Py_END_ALLOW_THREADS

if (rc == WAIT_FAILED) {
PyErr_SetFromWindowsErr(0);
goto error;
}

if (rc == WAIT_OBJECT_0) {
// Timer signaled: we are done
break;
}

assert(rc == (WAIT_OBJECT_0 + 1));
// The sleep was interrupted by SIGINT: restart sleeping
}
}
else {
DWORD rc;

Py_BEGIN_ALLOW_THREADS
rc = WaitForSingleObject(timer, INFINITE);
Py_END_ALLOW_THREADS

if (rc == WAIT_FAILED) {
PyErr_SetFromWindowsErr(0);
goto error;
}

assert(rc == WAIT_OBJECT_0);
// Timer signaled: we are done
}

CloseHandle(timer);
return 0;

error:
CloseHandle(timer);
return -1;
#endif
}
11 changes: 11 additions & 0 deletions Python/pytime.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
/* Conversion from nanoseconds */
#define NS_TO_MS (1000 * 1000)
#define NS_TO_US (1000)
#define NS_TO_100NS (100)


static void
Expand Down Expand Up @@ -568,6 +569,16 @@ _PyTime_AsNanoseconds(_PyTime_t t)
}


#ifdef MS_WINDOWS
_PyTime_t
_PyTime_As100Nanoseconds(_PyTime_t t, _PyTime_round_t round)
{
_PyTime_t ns = pytime_as_nanoseconds(t);
return pytime_divide(ns, NS_TO_100NS, round);
}
#endif


_PyTime_t
_PyTime_AsMicroseconds(_PyTime_t t, _PyTime_round_t round)
{
Expand Down