How To Make Python Wait

Posted by
on under

For many types of applications, at times it is necessary to pause the running of the program until some external condition occurs. You may need to wait until another thread finishes, or maybe until a new file appears in a directory on disk that is being watched.

In these and many other situations you will need to figure out a way to make your script wait, and this isn't as easy as it sounds if you want to do it properly! In this article I'm going to show you a few different ways to wait. I'm going to use Python for all the examples, but the concepts I'm going to present apply to all programming languages.

An Example Application That Waits

To show you these wait patterns, I'm going to use an example application, shown below:

from random import random
import threading
import time

result = None

def background_calculation():
    # here goes some long calculation
    time.sleep(random() * 5 * 60)

    # when the calculation is done, the result is stored in a global variable
    global result
    result = 42

def main():
    thread = threading.Thread(target=background_calculation)
    thread.start()

    # TODO: wait here for the result to be available before continuing!

    print('The result is', result)

if __name__ == '__main__':
    main()

In this application, the background_calculation() function performs some computation that is slow. To keep this example simple, I have coded this function with a time.sleep() call with a random time of up to 5 minutes. When the function reaches the end, a result global variable is set with the result of this made-up calculation, which is going to be, obviously, the number forty-two.

The main application function starts the background calculation in a separate thread, then waits for the thread to complete its work and finally prints the result global variable. The version of this function that you see above does not have the waiting part implemented, you can see a TODO comment in the place where the wait needs to take place. In the following sections I'm going to show you a few different ways to implement this wait, starting from the worst and working my way up to the best.

The Ugly: Busy Waiting

The easiest and most intuitive way to perform this wait is to use a while-loop:

    # wait here for the result to be available before continuing
    while result is None:
        pass

If you want to try this, here is the complete script that you can copy/paste:

from random import random
import threading
import time

result = None

def background_calculation():
    # here goes some long calculation
    time.sleep(random() * 5 * 60)

    # when the calculation is done, the result is stored in a global variable
    global result
    result = 42

def main():
    thread = threading.Thread(target=background_calculation)
    thread.start()

    # wait here for the result to be available before continuing
    while result is None:
        pass

    print('The result is', result)

if __name__ == '__main__':
    main()

This is a really bad way to wait. Can you tell why?

If you want to experience it, you can try the script on your system. As soon as the script runs, open your Task Manager on Windows, Activity Monitor on Mac or maybe top if you prefer the command line. Look at CPU usage and note how it goes through the roof.

This while-loop appears to be an empty loop, and in fact it mostly is, with the only exception that the loop exit condition needs to be checked over and over again to determine when the loop should exit. So while the loop body is completely empty, Python is forced to continuously evaluate result is None, and actually, the fact that the loop is empty makes Python concentrate fully on repeating this evaluation as fast as it possibly can, burning a lot of CPU cycles, and making everything else running on that CPU much slower!

This type of wait loop is often called a busy wait. And a CPU that is stuck doing a lot of work over nothing as in this case is said to be spinning. Never do this.

The Bad: Busy Waiting With Sleep

It is interesting that in the busy waiting example from the previous section, you would think that having an empty loop should give less work to the CPU, but in fact the contrary happens. So the obvious improvement to the previous solution is to add something inside the while-loop that puts a brake to the CPU frantically evaluating the while-loop exit condition.

I'm sure a lot of you can guess what we can do inside the loop to slow things down a bit. We can sleep:

    # wait here for the result to be available before continuing
    while result is None:
        time.sleep(15)

Here is the entire script, in case you want to run it locally:

from random import random
import threading
import time

result = None

def background_calculation():
    # here goes some long calculation
    time.sleep(random() * 5 * 60)

    # when the calculation is done, the result is stored in a global variable
    global result
    result = 42

def main():
    thread = threading.Thread(target=background_calculation)
    thread.start()

    # wait here for the result to be available before continuing
    while result is None:
        time.sleep(15)

    print('The result is', result)

if __name__ == '__main__':
    main()

The time.sleep() function will suspend the execution for the number of seconds passed in the argument. In the above example, we are sleeping for 15 seconds in each loop iteration, which means that Python will only evaluate the exit condition of the loop at a rate of four times per minute, compared to as fast as it could in the previous version. During those 15 seconds of sleep the CPU will not perform any work, and will be free to take on work from other processes running on your computer.

If you try this version of the wait, you are going to find that the script waits without overtasking the CPU, so you may think that we now have the perfect solution. Yet, I have titled this section "The Bad", didn't I?

While this solution is miles better than the previous one, there are two problems with it that still make it less than ideal. First of all, this loop still qualifies as busy waiting. It uses a lot less CPU than the previous one, but we still have a CPU that is spinning. We just made it tolerable by reducing the frequency of the spin.

The second problem is more concerning, in my opinion. Imagine the background task that is doing this calculation takes exactly 61 seconds to complete its work and produce a result. If our wait loop starts at about the same time the task starts, it would be checking the value of the result variable at 0, 15, 30, 45, 60 and 75 seconds. The check at 60 seconds would still return False because the background task still has one more second to go, so it will be the check at 75 seconds the causes the loop to exit. Can you see the problem? The loop exited at 75 seconds, but the background task finished at 61, so the wait extended for an extra 14 seconds!

While this type of wait is very common, it has this "resolution" problem, where the length of the wait is a multiple of the amount of sleep you do inside the loop. If you sleep less, then your wait time will be more accurate, but your CPU usage will go up due to busy waiting. If you sleep more, then you use less CPU, but you may end up waiting much longer than needed.

The Good #1: Joining the Thread

Let's say that we want our wait to be as efficient as possible. We want the wait to be over at the exact moment the result is produced by the calculation thread. How can we do that?

Solutions implemented just with Python logic like the previous two are not going to work, because to determine if the thread finished we need to run some Python code. If we run the check too often we use a lot of CPU, and if we run it not too often we will miss the exact moment the thread completed. We've seen this clearly in the previous two sections.

To be able to wait efficiently, we need external help from the operating system, which can efficiently notify our application when certain events occur. In particular, it can tell us when a thread exits, an operation that is called joining a thread.

The threading.Thread class from the Python standard library has a join() method that will return at the exact moment the thread exits:

    # wait here for the result to be available before continuing
    thread.join()

And here is the complete script:

from random import random
import threading
import time

result = None

def background_calculation():
    # here goes some long calculation
    time.sleep(random() * 5 * 60)

    # when the calculation is done, the result is stored in a global variable
    global result
    result = 42

def main():
    thread = threading.Thread(target=background_calculation)
    thread.start()

    # wait here for the result to be available before continuing
    thread.join()

    print('The result is', result)

if __name__ == '__main__':
    main()

The join() call blocks in the same way as time.sleep(), but instead of blocking for a fixed amount of time, it is going to block while the background thread runs. At the exact moment the thread finishes, the join() function is going to return, and the application can continue. The operating system makes doing an efficient wait a lot easier!

The Good #2: Waiting on an Event

If you need to wait for a thread to finish, the pattern I presented in the previous section is what you should use. But of course, there are many other situations in which you may need to wait for things other than threads, so how do you wait for some sort of ordinary event not tied to a thread or other operating system resource?

To show you how to do this, I'm going to modify the background thread in the example I've been using and make a bit more complex. This thread is still going to produce a result, but it is not going to exit immediately after that, it will continue running and doing some more work:

from random import random
import threading
import time

result = None

def background_calculation():
    # here goes some long calculation
    time.sleep(random() * 5 * 60)

    # when the calculation is done, the result is stored in a global variable
    global result
    result = 42

    # do some more work before exiting the thread
    time.sleep(10)

def main():
    thread = threading.Thread(target=background_calculation)
    thread.start()

    # wait here for the result to be available before continuing
    thread.join()

    print('The result is', result)

if __name__ == '__main__':
    main()

If you run the above version of the example, the result is going to be reported 10 seconds late, because the thread remains running for that long after generating the result. But of course we want to report the result at the exact moment it is available.

For situations like these, where you need to wait on an arbitrary condition, we can use an Event object, which comes in the threading package from the Python standard library. Here is how to create an event:

result_available = threading.Event()

Events have a wait() method, which we will use to write our wait:

    # wait here for the result to be available before continuing
    result_available.wait()

The difference between the Event.wait() and Thread.join() methods is that the latter is pre-programmed to wait for a specific event, which is the end of a thread. The former is a general purpose event that can wait on anything. So if this event object can wait on any condition, how do we tell it when to end the wait? For that, the event object has a set() method. Immediately after the background thread sets the result global variable it can set the event, causing any code waiting on it to unblock:

    # when the calculation is done, the result is stored in a global variable
    global result
    result = 42
    result_available.set()

Here is the complete code for this example:

from random import random
import threading
import time

result = None
result_available = threading.Event()

def background_calculation():
    # here goes some long calculation
    time.sleep(random() * 5 * 60)

    # when the calculation is done, the result is stored in a global variable
    global result
    result = 42
    result_available.set()

    # do some more work before exiting the thread
    time.sleep(10)

def main():
    thread = threading.Thread(target=background_calculation)
    thread.start()

    # wait here for the result to be available before continuing
    result_available.wait()

    print('The result is', result)

if __name__ == '__main__':
    main()

So here you can see how the background thread and the main thread are synchronized around this Event object.

The Good #3: Waiting While Displaying a Progress Percentage

One great thing about event objects is that they are general purpose, so you are going to find a lot of situations in which they can be useful if you apply a little bit of creativity. For example, consider this common pattern when writing a background thread function:

exit_thread = False

def background_thread():
    while not exit_thread:
        # do some work
        time.sleep(10)

Here we are trying to write a thread that can be terminated gracefully by setting the exit_thread global variable to True. This is a pretty common pattern, but by now you can probably identify why this isn't a great solution, right? It can take up to 10 seconds from the time the exit_thread variable is set until the thread actually exits, and that is without counting the extra time that may pass until the thread reaches the sleep statement.

We can write this in a much more efficient way using an Event object, taking advantage of the timeout argument that can be passed into the Event.wait() method:

exit_thread = threading.Event()

def background_thread():
    while True:
        # do some work
        if exit_thread.wait(timeout=10):
            break

With this implementation we have replaced a fixed-time sleep with a smart wait on an event object. We are still sleeping for 10 seconds at the end of each iteration, but if the thread is stuck in the exit_thread.wait(timeout=10) call at the exact moment the event's set() method is called from somewhere else, then the call will promptly return True and the thread will exit. If the timeout of 10 seconds is reached, then the wait() call returns False and the thread continues running the loop, so it is the same result as calling time.sleep(10).

If some other part of the program calls exit_thread.set() at a time where the thread is doing some work inside the loop, then the thread will continue running, but as soon as it reaches the exit_thread.wait() call it will return True immediately and exit. The secret to be able to terminate the thread without having to wait a lot is to make sure the event object is checked often enough.

Let me show you one more complete example using this timeout argument. What I'm going to do is take the code from the previous section and expand it to show a completion percentage while the wait is taking place.

First, let's add progress reporting to our background thread. In the original version, I slept for a random number of seconds up to a maximum of 300, which is 5 minutes. To report task progress during this time I'm going to replace the single sleep with a loop that runs 100 iterations sleeping a little bit in each, and this will give me the opportunity to report a progress percentage in each iteration. Since the big sleep went for up to 300 seconds, now I'm going to do 100 sleeps of up to 3 seconds each. Overall, this task will take the same amount of random time, but having the work partitioned in 100 pieces makes it easy to report a completion percentage.

Here are the changes to the background thread to report progress percentages in a progress global variable:

progress = 0

def background_calculation():
    # here goes some long calculation
    global progress
    for i in range(100):
        time.sleep(random() * 3)
        progress = i + 1

    # ...

And now we can build a more intelligent wait that reports the percentage of completion every 5 seconds:

    # wait here for the result to be available before continuing
    while not result_available.wait(timeout=5):
        print('\r{}% done...'.format(progress), end='', flush=True)
    print('\r{}% done...'.format(progress))

This new while loop is going to wait for the result_available event for up to 5 seconds as an exit condition. If nothing happens during this interval, then wait() is going to return False and we get inside the loop, where the current value of the progress variable is printed. Note that I use the \r character and the end='', flush=True arguments to the print() function to prevent the terminal from jumping to the next line. This trick allows you to print and reprint the same terminal line, so each progress line will print on top of the previous one.

As soon as the background calculation calls set() on the event object the loop is going to exit because wait() will immediately return True, and at this point I issue one more print, this time with the default end of line, so that I get the final percentage printed and the terminal is left ready to print the result on the next line.

Here is the complete code, if you want to run it or study it in more detail:

from random import random
import threading
import time

progress = 0
result = None
result_available = threading.Event()

def background_calculation():
    # here goes some long calculation
    global progress
    for i in range(100):
        time.sleep(random() * 3)
        progress = i + 1

    # when the calculation is done, the result is stored in a global variable
    global result
    result = 42
    result_available.set()

    # do some more work before exiting the thread
    time.sleep(10)

def main():
    thread = threading.Thread(target=background_calculation)
    thread.start()

    # wait here for the result to be available before continuing
    while not result_available.wait(timeout=5):
        print('\r{}% done...'.format(progress), end='', flush=True)
    print('\r{}% done...'.format(progress))

    print('The result is', result)

if __name__ == '__main__':
    main()

More Ways to Wait!

Event objects are not the only way to wait for events in your application, there are more ways, some of which may be more appropriate than events, depending on what you are waiting for.

If you need to watch a directory for files and act on the files as they are dropped there or when existing files are modified, an event is not going to be useful because the condition that should set the event is external to the application. In this case you need to use facilities provided by the operating system to watch for file system events. In Python, you can use the watchdog package, which wraps a few file watching APIs available in different operating systems.

If you need to wait for a subprocess to end, the subprocess package provides some functions to launch and wait for processes.

If you need to read from a network socket, the default configuration of the socket will make your read block until data arrives, so this works well as an efficient wait. If you need to wait on data arriving on multiple sockets or other file descriptors, then the select package from the Python standard library contains wrappers for operating system functions that can do this waiting efficiently.

If you want to write applications that produce and/or consume items of data, then you can use a Queue object. The producer adds items to the queue, while the consumer efficiently waits for items to take from it.

As you can see, in most cases, the operating system provides efficient wait mechanisms, so all you need to do is find how to access those from Python.

Waiting in Asyncio

If you are using the asyncio package, then you have access to similar types of waiting functions. For example, there are asyncio.Event and asyncio.Queue objects that are modeled after the original ones in the standard library, but based on the async/await style of programming.

Conclusion

I hope this article motivates you to think more carefully about how you wait in your applications. I suggest you play with all the examples I provided to familiarize with these techniques and eventually use them to replace inefficient time.sleep() calls in your code!

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

36 comments
  • #26 ak said

    Miguel: Consider this, the Thread class (can be multiple instances) is continuously monitoring for some log files on a specified path, if it finds the file it will do some processing, and while doing so if the main process is about to exit it will trigger end_process() on the spawned process before terminating the spawned process itself. And the spawned process should wait for the thread if it is processing files. (I tried to simulate this behavior with the above example.)
    Also, there will be some logic to clear(unset) the event if no one is listening. Maybe just before the Thread starts processing the logs.

    And the issue is the event is never received within end_process(). But is received when waiting(this code is commented in the example) within the run().

  • #27 Miguel Grinberg said

    @ak: sorry but I can't debug a program just from a description of it. Events do not happen asynchronously so they cannot interrupt code that is doing work, you have to check for them as often as you can. Maybe that's your problem?

  • #28 Soumitra said

    This is a fantastic article Miguel !
    Much thanks from California.

  • #29 Patrick said

    Hi,
    Thank you for that very detailed documentation. I'm very new to Python. I need to create a timer that is cancellable to launch a function only after 0.5 seconds of inactivity. I use keyboard commandes repeatedly, for example, UP and DOWN to scroll a list. I receive OSC data but there's no way to know when the data finished sending so i'm sure that 0.5 s is enough and then I launch my function. Which of the approaches above is the most efficient to achieve that? So, each time I press UP or DOWN, I send a timer and the timer resets itself every time until 0.5 second passed between the UP DOWN presses. English isn't my native language, I hope i'm clear enough! Everytjing I try (with threads and sleep()) are cumulative, I want the loop to be broken unless nothing is done for 0.5 second. Thanks a lot for your input on this!

  • #30 Miguel Grinberg said

    @Patrick: I think for your needs a Timer object will work nicely: https://docs.python.org/3/library/threading.html#timer-objects.

  • #31 Ashwani Paliwal said

    Great article.
    I was skimming through the internet and I came across this library Waiting. https://pypi.org/project/waiting/
    Would be really nice of you if you can compare this library with the method you have shared in this article.

  • #32 Miguel Grinberg said

    @Ashwani: I could not find a source repository for this package, which is very strange. That is a bad sign, I wouldn't use a package that does not have the code publicly available for inspection, submitting fixes, etc.

  • #33 Tom said

    Excellent post - was struggling to find something that clearly lays all this out, but this is brilliant. Than you!

  • #34 Binyamin Shmid said

    Beautiful thread, thank you so much!
    One important point I didn't see being mentioned is that
    Event.wait() not only works at the moment the event takes place but also if event happened already.

  • #35 Miguel Grinberg said

    @Binyamin: Yes, that is a very good point I did not mention in the article. Thanks!

  • #36 Nicolas Jaramillo said

    Thank you!!
    Very helpful guide. The "good #1" was perfect for my project. Thanks again!

Leave a Comment