Event loops

So far, we have implemented parallelism using OS threads. However, in many asynchronous frameworks, the coordination of concurrent tasks is managed by an event loop.

The idea behind an event loop is to continuously monitor the status of the various resources (for example, network connections and database queries) and trigger the execution of callbacks when events take place (for example, when a resource is ready or when a timer expires).

Why not just stick to threading?
Events loops are sometimes preferred as every unit of execution never runs at the same time as another and this can simplify dealing with shared variables, data structures, and resources. Read the next chapter for more details about parallel execution and its shortcomings.

As a first example, we will implement a thread-free version of threading.Timer. We can define a Timer class that will take a timeout and implement the Timer.done method that returns True if the timer has expired:

    class Timer:

def __init__(self, timeout):
self.timeout = timeout
self.start = time.time()

def done(self):
return time.time() - self.start > self.timeout

To determine whether the timer has expired, we can write a loop that continuously checks the timer status by calling the Timer.done method. When the timer expires, we can print a message and exit the cycle:

    timer = Timer(1.0)

while True:
if timer.done():
print("Timer is done!")
break

By implementing the timer in this way, the flow of execution is never blocked and we can, in principle, do other work inside the while loop.

Waiting for events to happen by continuously polling using a loop is commonly termed as busy-waiting.

Ideally, we would like to attach a custom function that executes when the timer goes off, just like we did in threading.Timer. To do this, we can implement a method, Timer.on_timer_done, that will accept a callback to be executed when the timer goes off:

    class Timer:
# ... previous code
def on_timer_done(self, callback):
self.callback = callback

Note that on_timer_done merely stores a reference to the callback. The entity that monitors the event and executes the callback is the loop. This concept is demonstrated as follows. Rather than using the print function, the loop will call timer.callback when appropriate:

    timer = Timer(1.0)
timer.on_timer_done(lambda: print("Timer is done!"))

while True:
if timer.done():
timer.callback()
break

As you can see, an asynchronous framework is starting to take place. All we did outside the loop was define the timer and the callback, while the loop took care of monitoring the timer and executing the associated callback. We can further extend our code by implementing support for multiple timers.

A natural way to implement multiple timers is to add a few Timer instances to a list and modify our event loop to periodically check all the timers and dispatch the callbacks when required. In the following code, we define two timers and attach a callback to each of them. Those timers are added to a list, timers, that is continuously monitored by our event loop. As soon as a timer is done, we execute the callback and remove the event from the list:

    timers = []

timer1 = Timer(1.0)
timer1.on_timer_done(lambda: print("First timer is done!"))

timer2 = Timer(2.0)
timer2.on_timer_done(lambda: print("Second timer is done!"))

timers.append(timer1)
timers.append(timer2)

while True:
for timer in timers:
if timer.done():
timer.callback()
timers.remove(timer)
# If no more timers are left, we exit the loop
if len(timers) == 0:
break

The main restriction of an event loop is, since the flow of execution is managed by a continuously running loop, that it never uses blocking calls. If we use any blocking statement (such as time.sleep) inside the loop, you can imagine how the event monitoring and callback dispatching will stop until the blocking call is done.

To avoid this, rather than using a blocking call, such as time.sleep, we let the event loop detect and execute the callback when the resource is ready. By not blocking the execution flow, the event loop is free to monitor multiple resources in a concurrent way.

The notification for events is usually implemented through operating system calls (such as the select Unix tool) that will resume the execution of the program whenever an event is ready (in contrast to busy-waiting).

The Python standard libraries include a very convenient event loop-based concurrency framework, asyncio, which will be the topic of the next section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.247.181