Skip to content

Commit

Permalink
bpo-21302: time.sleep() uses waitable timer on Windows (GH-28483)
Browse files Browse the repository at this point in the history
On Windows, 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).

* On Windows, time.sleep() now calls PyErr_CheckSignals() before
  resetting the SIGINT event.
* Add _PyTime_As100Nanoseconds() function.
* Complete and update time.sleep() documentation.

Co-authored-by: Livius <[email protected]>
  • Loading branch information
vstinner and Livius90 authored Sep 22, 2021
1 parent 8620be9 commit 58f8adf
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 56 deletions.
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

0 comments on commit 58f8adf

Please sign in to comment.