Asynchronous generators and the send method

How are generators relevant to our purposes of building an asynchronous server? The reason our current server cannot handle multiple clients is because the readline() function that we are using in the user business logic part, in order to obtain client data, is a blocking function that prevents the execution flow from going to other potential clients, as long as the current file object is still open. That is why, when the current client stops its connection with the server, the next client immediately receives the surge of information that we saw earlier.

If we could rewrite this function into an asynchronous one that allowed the execution flow to switch between different clients while those clients were all connecting to the server, that server would then become non-blocking. We will do this by using asynchronous generators to concurrently generate data from potentially multiple clients at the same time for our server.

To see the underlying structure of the asynchronous generator that we will use for our server, let's first consider the Chapter18/example5.py file, as follows:

# Chapter18/example5.py

import types

@types.coroutine
def read_data():
def inner(n):
try:
print(f'Printing from read_data(): {n}')
callback = gen.send(n * 2)
except StopIteration:
pass

data = yield inner
return data

async def process():
try:
while True:
data = await read_data()
print(f'Printing from process(): {data}')
finally:
print('Processing done.')

gen = process()
callback = gen.send(None)

def main():
for i in range(5):
print(f'Printing from main(): {i}')
callback(i)

if __name__ == '__main__':
main()

We are still considering the task of printing out multiples of 2, between 0 and 8. The process() function is our asynchronous generator in this example. You can see that there is, in fact, no yield keyword inside the generator; this is because we are using the await keyword, instead. This asynchronous generator is responsible for printing out the multiples of 2, computed by another generator, read_data().

The @types.coroutine decorator is used to convert the generator read_data() into a coroutine function that returns a generator-based coroutine, which can still be used as a regular generator but can also be awaited. This generator-based coroutine is the key to converting our blocking server to a non-blocking one. The coroutine performs the computation with the send() method, which is a way to provide a generator with input (in this case, we are providing the process() generator with multiples of 2).

This coroutine returns a callback, which can be called by our main program later. This is why, before looping through range(5) in the main program, we need to keep track of the process() generator itself (stored in the variable gen) and the callback that is returned (stored in the variable callback). The callback, specifically, is the return value of gen.send(None), which is used to start the execution of the process() generator. Finally, we simply loop over the aforementioned range object and call the callback object with the appropriate input.

A lot has been said about the theory behind this method of using asynchronous generators. Now, let's see it in action. Execute the program, and you should get the following output:

> python3 example5.py
Printing from main(): 0
Printing from read_data(): 0
Printing from process(): 0
Printing from main(): 1
Printing from read_data(): 1
Printing from process(): 2
Printing from main(): 2
Printing from read_data(): 2
Printing from process(): 4
Printing from main(): 3
Printing from read_data(): 3
Printing from process(): 6
Printing from main(): 4
Printing from read_data(): 4
Printing from process(): 8
Processing done.

In the output (specifically, the print statements), we can still observe the task switching events that are quintessential for both the asynchronous programming that was discussed in earlier chapters and the generators that produce output lazily. Essentially, we have achieved the same goal as the previous example (printing multiples of 2), but here, we used asynchronous generators (with the async and await keywords) to facilitate task switching events, and we were also able to pass specific arguments to generators by using a callback. These techniques, when combined, form the basic structure that will be applied to our currently blocking server.

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

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