Python time.sleep() vs event.wait()
Solution 1
Using exit_flag.wait(timeout=DELAY)
will be more responsive, because you'll break out of the while loop instantly when exit_flag
is set. With time.sleep
, even after the event is set, you're going to wait around in the time.sleep
call until you've slept for DELAY
seconds.
In terms of implementation, Python 2.x and Python 3.x have very different behavior. In Python 2.x Event.wait
is implemented in pure Python using a bunch of small time.sleep
calls:
from time import time as _time, sleep as _sleep
....
# This is inside the Condition class (Event.wait calls Condition.wait).
def wait(self, timeout=None):
if not self._is_owned():
raise RuntimeError("cannot wait on un-acquired lock")
waiter = _allocate_lock()
waiter.acquire()
self.__waiters.append(waiter)
saved_state = self._release_save()
try: # restore state no matter what (e.g., KeyboardInterrupt)
if timeout is None:
waiter.acquire()
if __debug__:
self._note("%s.wait(): got it", self)
else:
# Balancing act: We can't afford a pure busy loop, so we
# have to sleep; but if we sleep the whole timeout time,
# we'll be unresponsive. The scheme here sleeps very
# little at first, longer as time goes on, but never longer
# than 20 times per second (or the timeout time remaining).
endtime = _time() + timeout
delay = 0.0005 # 500 us -> initial delay of 1 ms
while True:
gotit = waiter.acquire(0)
if gotit:
break
remaining = endtime - _time()
if remaining <= 0:
break
delay = min(delay * 2, remaining, .05)
_sleep(delay)
if not gotit:
if __debug__:
self._note("%s.wait(%s): timed out", self, timeout)
try:
self.__waiters.remove(waiter)
except ValueError:
pass
else:
if __debug__:
self._note("%s.wait(%s): got it", self, timeout)
finally:
self._acquire_restore(saved_state)
This actually means using wait
is probably a bit more CPU-hungry than just sleeping the full DELAY
unconditionally, but has the benefit being (potentially a lot, depending on how long DELAY
is) more responsive. It also means that the GIL needs to be frequently re-acquired, so that the next sleep can be scheduled, while time.sleep
can release the GIL for the full DELAY
. Now, will acquiring the GIL more frequently have a noticeable effect on other threads in your application? Maybe or maybe not. It depends on how many other threads are running and what kind of work loads they have. My guess is it won't be particularly noticeable unless you have a high number of threads, or perhaps another thread doing lots of CPU-bound work, but its easy enough to try it both ways and see.
In Python 3.x, much of the implementation is moved to pure C code:
import _thread # C-module
_allocate_lock = _thread.allocate_lock
class Condition:
...
def wait(self, timeout=None):
if not self._is_owned():
raise RuntimeError("cannot wait on un-acquired lock")
waiter = _allocate_lock()
waiter.acquire()
self._waiters.append(waiter)
saved_state = self._release_save()
gotit = False
try: # restore state no matter what (e.g., KeyboardInterrupt)
if timeout is None:
waiter.acquire()
gotit = True
else:
if timeout > 0:
gotit = waiter.acquire(True, timeout) # This calls C code
else:
gotit = waiter.acquire(False)
return gotit
finally:
self._acquire_restore(saved_state)
if not gotit:
try:
self._waiters.remove(waiter)
except ValueError:
pass
class Event:
def __init__(self):
self._cond = Condition(Lock())
self._flag = False
def wait(self, timeout=None):
self._cond.acquire()
try:
signaled = self._flag
if not signaled:
signaled = self._cond.wait(timeout)
return signaled
finally:
self._cond.release()
And the C code that acquires the lock:
/* Helper to acquire an interruptible lock with a timeout. If the lock acquire
* is interrupted, signal handlers are run, and if they raise an exception,
* PY_LOCK_INTR is returned. Otherwise, PY_LOCK_ACQUIRED or PY_LOCK_FAILURE
* are returned, depending on whether the lock can be acquired withing the
* timeout.
*/
static PyLockStatus
acquire_timed(PyThread_type_lock lock, PY_TIMEOUT_T microseconds)
{
PyLockStatus r;
_PyTime_timeval curtime;
_PyTime_timeval endtime;
if (microseconds > 0) {
_PyTime_gettimeofday(&endtime);
endtime.tv_sec += microseconds / (1000 * 1000);
endtime.tv_usec += microseconds % (1000 * 1000);
}
do {
/* first a simple non-blocking try without releasing the GIL */
r = PyThread_acquire_lock_timed(lock, 0, 0);
if (r == PY_LOCK_FAILURE && microseconds != 0) {
Py_BEGIN_ALLOW_THREADS // GIL is released here
r = PyThread_acquire_lock_timed(lock, microseconds, 1);
Py_END_ALLOW_THREADS
}
if (r == PY_LOCK_INTR) {
/* Run signal handlers if we were interrupted. Propagate
* exceptions from signal handlers, such as KeyboardInterrupt, by
* passing up PY_LOCK_INTR. */
if (Py_MakePendingCalls() < 0) {
return PY_LOCK_INTR;
}
/* If we're using a timeout, recompute the timeout after processing
* signals, since those can take time. */
if (microseconds > 0) {
_PyTime_gettimeofday(&curtime);
microseconds = ((endtime.tv_sec - curtime.tv_sec) * 1000000 +
(endtime.tv_usec - curtime.tv_usec));
/* Check for negative values, since those mean block forever.
*/
if (microseconds <= 0) {
r = PY_LOCK_FAILURE;
}
}
}
} while (r == PY_LOCK_INTR); /* Retry if we were interrupted. */
return r;
}
This implementation is responsive, and doesn't require frequent wakeups that re-acquire the GIL, so you get the best of both worlds.
Solution 2
Python 2.*
Like @dano said, event.wait is more responsive,
but it can be dangerous when the system time is changed backward, while it's waiting!
bug# 1607041: Condition.wait timeout fails on clock change
See this sample:
def someHandler():
while not exit_flag.wait(timeout=0.100):
action()
Normally action()
will be called in a 100ms intrvall.
But when you change the time ex. one hour then there is a pause of one hour between two actions.
Conclusion: When it's allowed that the time can be change, you should avoid event.wait
, it can be disastrous!
Python 3 uses a monotonic clock to implement timeouts, so it's solved there
Solution 3
It is interesting to note that the event.wait() method can be invoked on its own:
from threading import Event # Needed for the wait() method
from time import sleep
print("\n Live long and prosper!")
sleep(1) # Conventional sleep() Method.
print("\n Just let that soak in..")
Event().wait(3.0) # wait() Method, useable sans thread.
print("\n Make it So! = )\n")
So why -not- use wait() as an alternative to sleep() outside of multi-threading? In a word, Zen. (Of course.) Clarity of code is an important thing.
Related videos on Youtube

Sunilsingh
Realtime Embedded Systems Programmer. Builder of Robots, Rockets, Reverb, and other Real-Time randomness.
Updated on December 23, 2021Comments
-
Sunilsingh 12 months
I want to perform an action at a regular interval in my multi-threaded Python application. I have seen two different ways of doing it
exit = False def thread_func(): while not exit: action() time.sleep(DELAY)
or
exit_flag = threading.Event() def thread_func(): while not exit_flag.wait(timeout=DELAY): action()
Is there an advantage to one way over the other? Does one use less resources, or play nicer with other threads and the GIL? Which one makes the remaining threads in my app more responsive?
(Assume some external event sets
exit
orexit_flag
, and I am willing to wait the full delay while shutting down)-
tdelaney almost 8 yearsWhere is the code that sets the
exit
flag? Is it in theaction()
call or in another thread or maybe called by a signal handler? -
tdelaney almost 8 yearsI use
Event.wait
in this situation even though python 2.x is polling in the background. Sleeping at, say, 1 second intervals is reasonably responsive and less intrusive. -
user253751 almost 8 yearsThe first one is going to waste some CPU time, for one thing.
-
Ognjen over 7 yearsinterresting side-effect of Event.wait. I'm reverse-engeneering the python API of an application which has an embedded python 2.5 interpreter ( ableton live ), and the parent process doesn't like python threads in some way, maybe it's only running when processing an event, making the rconsole I injected irresponsive. If I loop over time.sleep, it's still unresponsive, but if I use event.wait with a timeout in the main thread, the parent app is still responding and rconsole is reasonably responsive.
-
-
user3012759 almost 8 yearsso, does it mean that the
sleep(DELAY)
is less GIL heavy? albeit not as accurate? -
dano almost 8 years@user3012759 I would think so, since each wakeup inside of
wait
would require re-acquiring the GIL, wheresleep
can just release it for the entirety ofDELAY
. -
tdelaney almost 8 yearsThis is python 2.x (its considerably better in 3.x) and its pretty bad especially as the number of threads increases.
-
user3012759 almost 8 years@tdelaney how does 3.x impl look like?
-
dano almost 8 years@tdelaney Yes, good point. In Python 3.x the wait is implemented in C, and releases the GIL for the entirety of the wait. I'll update my answer to show the code
-
Fahad Alduraibi over 1 yearNo, sleep doesn't block other threads, it only blocks the thread it is running in.
-
Jamal Alkelani over 1 year@FahadAlduraibi I know it's blocking only the thread it's running in, but based on the GIL GLOBAL INTERPRETER LOCK, it will badly affect the other running threads! because the sleep method is consuming the resources while wait doesn't
-
lys 12 monthsThis is really interesting to know - but would be good to clarify if only Python 2.* is affected
-
jeb 12 months@lys It's fixed in 3.*, I updated my answer