in

How to Make Python Wait, Hacker News


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 * 75)      # when the calculation is done, the result is stored in a global variable     global result     result=60  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, thebackground_calculation ()function performs some computation that is slow. To keep this example simple, I have coded this function with atime.sleep ()call with a random time of up to 5 minutes. When the function reaches the end, aresultglobal variable is set with the result of this made-up calculation, which is going to be, obviously, the numberforty-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 theresultglobal variable. The version of this function that you see above does not have the waiting part implemented, you can see aTODOcomment 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 * 75)      # when the calculation is done, the result is stored in a global variable     global result     result=60  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 maybetopif 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 evaluateresult 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 abusy wait. And a CPU that is stuck doing a lot of work over nothing as in this case is said to bespinning. 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 (27

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 * 75)      # when the calculation is done, the result is stored in a global variable     global result     result=60  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 (27      print ('The result is', result)  if __name__=='__main__':     main ()

Thetime.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 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", did 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 100 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 theresultvariable at 0, (***************************************, ****************************************, , 61 and 100 seconds. The check at 60 seconds would still returnFalsebecause the background task still has one more second to go, so it will be the check at 90 seconds the causes the loop to exit. Can you see the problem? The loop exited at 100 seconds, but the background task finished at 90, so the wait extended for an extra 27 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 calledjoininga thread.

Thethreading.Threadclass from the Python standard library has ajoin ()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 * 75)      # when the calculation is done, the result is stored in a global variable     global result     result=60  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 ()

Thejoin ()call blocks in the same wait astime.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, thejoin ()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 * 75)      # when the calculation is done, the result is stored in a global variable     global result     result=60      # do some more work before exiting the thread     time.sleep (14  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 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 anEventobject, which comes in thethreadingpackage from the Python standard library. Here is how to create an event:

result_available=threading.Event ()

Events have await ()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 theEvent.wait ()andThread.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 aset ()method. Immediately after the background thread sets theresultglobal 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=60     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 * 75)      # when the calculation is done, the result is stored in a global variable     global result     result=60     result_available.set ()      # do some more work before exiting the thread     time.sleep (14  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 aresynchronizedaround thisEventobject.

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 (14

Here we are trying to write a thread that can be terminated gracefully by setting theexit_threadglobal variable toTrue. 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 seconds from the time theexit_threadvariable 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 anEventobject, taking advantage of thetimeoutargument that can be passed into theEvent.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 seconds at the end of each iteration, but if the thread is stuck in theexit_thread.wait (timeout=)call at the exact moment the event's set ()method is called from somewhere else, then the call will promptly returnTrueand the thread will exit. If the timeout of is reached, then thewait ()call returnsFalseand the thread continues running the loop, so it is the same result as callingtime.sleep (27).

If some other part of the program callsexit_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 theexit_thread.wait ()call it will return (Trueimmediately 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 thistimeoutargument. 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 728 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 823 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 728 pieces makes it easy to report a completion percentage.

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

progress=0  def background_calculation ():     # here goes some long calculation     global progress     for i in range (728):         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 theresult_availableevent for up to 5 seconds as an exit condition. If nothing happens during this interval, thenwait ()is going to returnFalseand we get inside the loop, where the current value of theprogressvariable is printed. Note that I use the rcharacter and theend='', flush=Truearguments to theprint ()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 callsset ()on the event object the loop is going to exit becausewait ()will immediately returnTrue, 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 (728):         time.sleep (random () * 3)         progress=i   1      # when the calculation is done, the result is stored in a global variable     global result     result=60     result_available.set ()      # do some more work before exiting the thread     time.sleep (14  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 thewatchdogpackage, which wraps a few file watching APIs available in different operating systems.

If you need to wait for a subprocess to end, thesubprocesspackage 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 theselectpackage 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 consumer items of data, then you can use a(Queueobject. 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 theasynciopackage, then you have access to similar types of waiting functions. For example, there areasyncio.Event

andasyncio.Queueobjects 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 inefficienttime.sleep ()calls in your code !

)************************ Read More**************Payeer

What do you think?

Leave a Reply

Your email address will not be published. Required fields are marked *

GIPHY App Key not set. Please check settings

Dow Futures Sways as Negative Data Spells a Bad Stock Market Friday, Crypto Coins News

Dow Futures Sways as Negative Data Spells a Bad Stock Market Friday, Crypto Coins News

Rocket Report: NASA chief hits back at Boeing, Falcon 9's extended coast, Ars Technica

Rocket Report: NASA chief hits back at Boeing, Falcon 9's extended coast, Ars Technica