From 58f8adfda3c2b42f654a55500e8e3a6433cb95f2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 22 Sep 2021 16:09:30 +0200 Subject: [PATCH] bpo-21302: time.sleep() uses waitable timer on Windows (GH-28483) 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 --- Doc/library/time.rst | 33 ++-- Doc/whatsnew/3.11.rst | 11 +- Include/cpython/pytime.h | 6 + .../2021-09-20-22-46-40.bpo-21302.h56430.rst | 3 + Modules/timemodule.c | 153 +++++++++++++----- Python/pytime.c | 11 ++ 6 files changed, 161 insertions(+), 56 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-09-20-22-46-40.bpo-21302.h56430.rst diff --git a/Doc/library/time.rst b/Doc/library/time.rst index 34cb28f1a3cd3e..d91862cc38be83 100644 --- a/Doc/library/time.rst +++ b/Doc/library/time.rst @@ -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 diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index acc00d867d00c8..12e46c3edcde27 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -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 diff --git a/Include/cpython/pytime.h b/Include/cpython/pytime.h index b0453884398cbf..8c2958501f7967 100644 --- a/Include/cpython/pytime.h +++ b/Include/cpython/pytime.h @@ -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); diff --git a/Misc/NEWS.d/next/Library/2021-09-20-22-46-40.bpo-21302.h56430.rst b/Misc/NEWS.d/next/Library/2021-09-20-22-46-40.bpo-21302.h56430.rst new file mode 100644 index 00000000000000..22011b791e5f2d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-09-20-22-46-40.bpo-21302.h56430.rst @@ -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. diff --git a/Modules/timemodule.c b/Modules/timemodule.c index 52c61154f3a05b..53ec86eb3981ef 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -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; } @@ -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); @@ -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()) { @@ -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 } diff --git a/Python/pytime.c b/Python/pytime.c index 8035a5f8a28b48..7f9f301f72090f 100644 --- a/Python/pytime.c +++ b/Python/pytime.c @@ -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 @@ -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) {