\[ [Index](index.md) | [Exercise 8.4](ex8_4.md) | [Exercise 8.6](ex8_6.md) \] # Exercise 8.5 *Objectives:* - Learn about managed generators *Files Created:* `multitask.py`, `server.py` A generator or coroutine function can never execute without being driven by some other code. For example, a generator used for iteration doesn't do anything unless iteration is actually carried out using a for-loop. Similarly, a collection of coroutines won't run unless their `send()` method is invoked somehow. In advanced applications of generators, it is possible to drive generators in various unusual ways. In this exercise, we look at a few examples. ## (a) Generators as tasks If a file `multitask.py`, define the following code: ```python # multitask.py from collections import deque tasks = deque() def run(): while tasks: task = tasks.popleft() try: task.send(None) tasks.append(task) except StopIteration: print('Task done') ``` This code implements a tiny task scheduler that runs generator functions. Try it by running it on the following functions. ```python # multitask.py ... def countdown(n): while n > 0: print('T-minus', n) yield n -= 1 def countup(n): x = 0 while x < n: print('Up we go', x) yield x += 1 if __name__ == '__main__': tasks.append(countdown(10)) tasks.append(countdown(5)) tasks.append(countup(20)) run() ``` When you run this, you should see output from all of the generators interleaved together. For example: ```python T-minus 10 T-minus 5 Up we go 0 T-minus 9 T-minus 4 Up we go 1 T-minus 8 T-minus 3 Up we go 2 T-minus 7 T-minus 2 Up we go 3 T-minus 6 T-minus 1 Up we go 4 T-minus 5 Task done Up we go 5 T-minus 4 Up we go 6 T-minus 3 Up we go 7 T-minus 2 Up we go 8 T-minus 1 Up we go 9 Task done Up we go 10 Up we go 11 Up we go 12 Up we go 13 Up we go 14 Up we go 15 Up we go 16 Up we go 17 Up we go 18 Up we go 19 Task done ``` That's interesting, but not especially compelling. Move on to the next example. ## (b) Generators as Tasks Serving Network Connections Create a file `server.py` and put the following code into it: ```python # server.py from socket import * from select import select from collections import deque tasks = deque() recv_wait = {} # sock -> task send_wait = {} # sock -> task def run(): while any([tasks, recv_wait, send_wait]): while not tasks: can_recv, can_send, _ = select(recv_wait, send_wait, []) for s in can_recv: tasks.append(recv_wait.pop(s)) for s in can_send: tasks.append(send_wait.pop(s)) task = tasks.popleft() try: reason, resource = task.send(None) if reason == 'recv': recv_wait[resource] = task elif reason == 'send': send_wait[resource] = task else: raise RuntimeError('Unknown reason %r' % reason) except StopIteration: print('Task done') ``` This code is a slightly more complicated version of the task scheduler in part (a). It will require a bit of study, but the idea is that not only will each task yield, it will indicate a reason for doing so (receiving or sending). Depending on the reason, the task will move over to a waiting area. The scheduler then runs any available tasks or waits for I/O events to occur when nothing is left to do. It's all a bit tricky perhaps, but add the following code which implements a simple echo server: ```python # server.py ... def tcp_server(address, handler): sock = socket(AF_INET, SOCK_STREAM) sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) sock.bind(address) sock.listen(5) while True: yield 'recv', sock client, addr = sock.accept() tasks.append(handler(client, addr)) def echo_handler(client, address): print('Connection from', address) while True: yield 'recv', client data = client.recv(1000) if not data: break yield 'send', client client.send(b'GOT:' + data) print('Connection closed') if __name__ == '__main__': tasks.append(tcp_server(('',25000), echo_handler)) run() ``` Run this server in its own terminal window. In another terminal, connect to it using a command such as `telnet` or `nc`. For example: ``` bash % nc localhost 25000 Hello Got: Hello World Got: World ``` If you don't have access to `nc` or `telnet` you can also use Python itself: ``` bash % python3 -m telnetlib localhost 25000 Hello Got: Hello World Got: World ``` If it's working, you should see output being echoed back to you. Not only that, if you connect multiple clients, they'll all operate concurrently. This tricky use of generators is not something that you would likely have to code directly. However, they are used in certain advanced packages such as `asyncio` that was added to the standard library in Python 3.4. \[ [Solution](soln8_5.md) | [Index](index.md) | [Exercise 8.4](ex8_4.md) | [Exercise 8.6](ex8_6.md) \] ---- `>>>` Advanced Python Mastery `...` A course by [dabeaz](https://www.dabeaz.com) `...` Copyright 2007-2023 ![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)