Callbacks

The code we have seen so far blocks the execution of the program until the resource is available. The call responsible for the waiting is time.sleep. To make the code start working on other tasks, we need to find a way to avoid blocking the program flow so that the rest of the program can go on with the other tasks.

One of the simplest ways to accomplish this behavior is through callbacks. The strategy is quite similar to what we do when we request a cab.

Imagine that you are at a restaurant and you've had a few drinks. It's raining outside, and you'd rather not take the bus; therefore, you request a taxi and ask them to call when they're outside so that you can come out, and you don't have to wait in the rain.

What you did in this case is request a taxi (that is, the slow resource) but instead of waiting outside until the taxi arrives, you provide your number and instructions (callback) so that you can come outside when they're ready and go home.

We will now show how this mechanism can work in code. We will compare the blocking code of time.sleep with the equivalent non-blocking code of threading.Timer.

For this example, we will write a function, wait_and_print, that will block the program execution for one second and then print a message:

    def wait_and_print(msg):
time.sleep(1.0)
print(msg)

If we want to write the same function in a non-blocking way, we can use the threading.Timer class. We can initialize a threading.Timer instance by passing the amount of time we want to wait and a callback. A callback is simply a function that will be called when the timer expires. Note that we have to also call the Timer.start method to activate the timer:

    import threading

def wait_and_print_async(msg):
def callback():
print(msg)

timer = threading.Timer(1.0, callback)
timer.start()

An important feature of the wait_and_print_async function is that none of the statements are blocking the execution flow of the program.

How is threading.Timer capable of waiting without blocking?
The strategy used by threading.Timer involves starting a new thread that is able to execute code in parallel. If this is confusing, don't worry, we will explore threading and parallel programming in detail in the following chapters.

This technique of registering callbacks for execution in response to certain events is commonly called the Hollywood principle. This is because, after an audition for a role at Hollywood, you may be told "Don't call us, we'll call you", meaning that they won't tell you if they chose you for the role immediately, but they'll call you in case they do.

To highlight the difference between the blocking and non-blocking version of wait_and_print, we can test and compare the execution of the two versions. In the output comments, the waiting periods are indicated by <wait...>:

    # Syncronous
wait_and_print("First call")
wait_and_print("Second call")
print("After call")
# Output:
# <wait...>
# First call
# <wait...>
# Second call
# After call
# Async
wait_and_print_async("First call async")
wait_and_print_async("Second call async")
print("After submission")
# Output:
# After submission
# <wait...>
# First call
# Second call

The synchronous version behaves in a very familiar way. The code waits for a second, prints First call, waits for another second, and then prints the Second call and After call messages.

In the asynchronous version, wait_and_print_async submits  (rather than execute)  those calls and moves on immediately. You can see this mechanism in action by acknowledging that the "After submission" message is printed immediately.

With this in mind, we can explore a slightly more complex situation by rewriting our network_request function using callbacks. In the following code, we define the network_request_async function. The biggest difference between network_request_async and its blocking counterpart is that network_request_async doesn't return anything. This is because we are merely submitting the request when network_request_async is called, but the value is available only when the request is completed.

If we can't return anything, how do we pass the result of the request? Rather than returning the value, we will pass the result as an argument to the on_done callback.

The rest of the function consists of submitting a callback (called timer_done) to the timer.Timer class that will call on_done when it's ready:

    def network_request_async(number, on_done):

def timer_done():
on_done({"success": True,
"result": number ** 2})

timer = threading.Timer(1.0, timer_done)
timer.start()

The usage of network_request_async is quite similar to timer.Timer; all we have to do is pass the number we want to square and a callback that will receive the result when it's ready. This is demonstrated in the following snippet:

    def on_done(result):
print(result)

network_request_async(2, on_done)

Now, if we submit multiple network requests, we note that the calls get executed concurrently and do not block the code:

    network_request_async(2, on_done)
network_request_async(3, on_done)
network_request_async(4, on_done)
print("After submission")

In order to use network_request_async in fetch_square, we need to adapt the code to use asynchronous constructs. In the following code, we modify fetch_square by defining and passing the on_done callback to network_request_async:

    def fetch_square(number):
def on_done(response):
if response["success"]:
print("Result is: {}".format(response["result"]))

network_request_async(number, on_done)

You may have noted that the asynchronous code is significantly more convoluted than its synchronous counterpart. This is due to the fact that we are required to write and pass a callback every time we need to retrieve a certain result, causing the code to become nested and hard to follow.

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

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