Skip to content
This repository was archived by the owner on May 31, 2021. It is now read-only.

add example explaining concurrent programming with coroutines #15

Closed
wrobell opened this issue Nov 1, 2016 · 8 comments
Closed

add example explaining concurrent programming with coroutines #15

wrobell opened this issue Nov 1, 2016 · 8 comments

Comments

@wrobell
Copy link

wrobell commented Nov 1, 2016

I wonder if the following example should be included into the docs to explain concurrent programming with coroutines.

The code uses generator coroutine syntax and is easy to follow

  • the entry and exit points of coroutines are clearly visible
  • therefore it is possible to explain why the code is concurrent and how it works
  • problem with blocking calls can be described
  • it contains parts, which could be moved to separate function - custom event loop
  • the example is in one file (as opposed to tcp echo example)

After describing the example, it could be folded into async/await version running on asyncio event loop. Benefits of asyncio event loop can be shown, i.e.

  • simplified programming
  • other async coroutines can still run, while a socket is waiting for data

Would that make sense?

import socket
import time

PORT = 10000
N = 5

def sender(name):
    s = socket.socket()
    s.connect(('', PORT))
    name = name.encode()
    while True:
        data = yield 
        s.send(name + b'|' + data.encode())

def receiver(client):
    while True:
        yield
        value = client.recv(40)
        client_id, value = value.decode().split('|')
        msg = 'client {}: {}'.format(client_id, value)
        print(msg)


server = socket.socket()
server.bind(('', PORT))
server.listen()

senders = []
receivers = []
for i in range(N):
    s = sender('number {}'.format(i))
    s.send(None)

    c, _ = server.accept()
    r = receiver(c)
    r.send(None)

    senders.append(s)
    receivers.append(r)

for i in range(10):
    for s in senders:
        s.send('item {:03d}'.format(i))
    for r in receivers:
        r.send(None)
    print()
    time.sleep(1)
@vxgmichel
Copy link
Collaborator

Re-implementing asyncio from scratch requires:

  • a selector to handle IO operations
  • an event loop to schedule callbacks
  • futures to represent asynchronous results
  • tasks to run the coroutines

All those concepts are more or less interdependent. I don't see how to explain asyncio coroutines without tasks + futures + event loop, and single-threaded concurrency without a selector.

It's an interesting exercise though, I wonder how long a basic asyncio implementation could be.

@wrobell
Copy link
Author

wrobell commented Nov 1, 2016

This is more about concurrent programming model with coroutines than asyncio itself. As you can see, you can run sender and receiver coroutines concurrently and no selector is involved. The example uses sockets, but any other non-IO program could be implemented to explain the model (i.e. the program presented by Knuth in his book).

@vxgmichel
Copy link
Collaborator

vxgmichel commented Nov 2, 2016

@wrobell

As you can see, you can run sender and receiver coroutines concurrently and no selector is involved.

Coroutines alone are not enough to provide concurrency. Rather, they can be used along with a cooperative scheduler (i.e an event loop) to implement cooperative multitasking. That also means you need a protocol for the coroutines and the scheduler to communicate. In asyncio, this is done by yielding from a future, which means "please wake me up when this operation is finished".

In your example, you can easily replace the coroutines with regular classes, and it would work the same:

class sender:
    def __init__(self, name):
        self.s = socket.socket()
        self.s.connect(('', PORT))
        self.name = name.encode()

    def send(self, data):
        self.s.send(self.name + b'|' + data.encode())

class receiver:
    def __init__(self, client):
        self.client = client

    def receive(self):
        value = self.client.recv(40)
        client_id, value = value.decode().split('|')
        msg = 'client {}: {}'.format(client_id, value)
        print(msg)

@wrobell
Copy link
Author

wrobell commented Nov 2, 2016

Probably we are splitting hairs over definitions here a bit. You can replace "concurrency" with "multitasking" (maybe wording of https://en.wikipedia.org/wiki/Coroutine and http://www.dabeaz.com/coroutines/ will help to explain my position).

You can use classes of course, but then you have to maintain state of your processing on object level - global from class method perspective. Every example provided in presentations at http://www.dabeaz.com/coroutines/ can be reimplemented with classes. Async I/O can be reimplemented with callbacks. But I believe we all agree that coroutines provide better programming model, so we use them and IMHO there is no point to discuss this. :)

It seems we disagree on the need of event loop. Point of my example is that there is no event loop. You can analyze the code of the example and see when and what happens - there is no magic of event loop. It is illustration of multitasking (or concurrency or cooperative execution) with coroutines. Nothing else, nothing more.

The programming model is provided by Python language coroutines. Asyncio uses the model to enable asynchronous I/O programming. IMHO, if we first explain the former, the latter will be easier to understand.

@vxgmichel
Copy link
Collaborator

vxgmichel commented Nov 2, 2016

@wrobell

You can replace "concurrency" with "multitasking"

Yes I used them as synonyms, sorry if I didn't make that clear.

But I believe we all agree that coroutines provide better programming model, so we use them and IMHO there is no point to discuss this. :)

Indeed!

It seems we disagree on the need of event loop. Point of my example is that there is no event loop. You can analyze the code of the example and see when and what happens - there is no magic of event loop. It is illustration of multitasking (or concurrency or cooperative execution) with coroutines.

What bothers me in your example is this part:

for i in range(10):
    for s in senders:
        s.send('item {:03d}'.format(i))
    for r in receivers:
        r.send(None)

Things here are executed in a very strict order, so it looks much more like serialized calls than concurrent tasks. I wouldn't call that concurrency, but as you said, it might be a matter of definition.

Anyway, this is how I would illustrate concurrency using coroutines instead.

@wrobell
Copy link
Author

wrobell commented Nov 15, 2016

Indeed, the order is quite strict and non-coroutine part of my example is quite long. Would be nice to have shorter example to analyze.

BTW. The following article explains coroutines in Python. If asyncio docs do not have its own description of coroutines, then maybe it would be worth to link to it?
https://jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/

@vxgmichel
Copy link
Collaborator

vxgmichel commented Nov 15, 2016

That part of the article is quite interesting:

Outside of Python, all but the simplest generators would be referred to as coroutines. I'll use the latter term later in the post. The important thing to remember is, in Python, everything described here as a coroutine is still a generator. Python formally defines the term generator; coroutine is used in discussion but has no formal definition in the language.

Now keep in mind that this article is from 2013, and it was only the beginning of asyncio. Here's what PEP 3156 says about coroutines:

A coroutine is a generator that follows certain conventions. For documentation purposes, all coroutines should be decorated with @asyncio.coroutine , but this cannot be strictly enforced.

Then, in 2015, PEP 492 introduced async/await and coroutines became a proper standalone concept. Python now makes a clear separation between generators and coroutines, since they're different (yet related) concepts used for different purposes. I think we should stick to:

# Native coroutine
async def coro():
    await some_awaitable()

# Generator-based coroutine
@asyncio.coroutine
def coro():
    yield from some_awaitable()

# Since python 3.6: Asynchronous generators
async def gen():
    await some_awaitable()
    yield 1

and call everything else using yield or yield from a generator (or enhanced generator), to avoid confusion.

In my opinion, this article by Brett Cannon would be a better fit for the documentation:

@vstinner
Copy link
Contributor

I shut down the project: #33

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants