[Python-checkins] bpo-21302: time.sleep() uses waitable timer on Windows (GH-28483)

vstinner webhook-mailer at python.org
Wed Sep 22 10:09:39 EDT 2021


https://github.com/python/cpython/commit/58f8adfda3c2b42f654a55500e8e3a6433cb95f2
commit: 58f8adfda3c2b42f654a55500e8e3a6433cb95f2
branch: main
author: Victor Stinner <vstinner at python.org>
committer: vstinner <vstinner at python.org>
date: 2021-09-22T16:09:30+02:00
summary:

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 <egyszeregy at freemail.hu>

files:
A Misc/NEWS.d/next/Library/2021-09-20-22-46-40.bpo-21302.h56430.rst
M Doc/library/time.rst
M Doc/whatsnew/3.11.rst
M Include/cpython/pytime.h
M Modules/timemodule.c
M Python/pytime.c

diff --git a/Doc/library/time.rst b/Doc/library/time.rst
index 34cb28f1a3cd3..d91862cc38be8 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 acc00d867d00c..12e46c3edcde2 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 b0453884398cb..8c2958501f796 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 0000000000000..22011b791e5f2
--- /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 52c61154f3a05..53ec86eb3981e 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 8035a5f8a28b4..7f9f301f72090 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)
 {



More information about the Python-checkins mailing list