In [9]:
import asyncio
import aiohttp

loop = asyncio.get_event_loop()

presenter = {'name': 'Mantas Zimnickas'}
In [1]:
import asyncio
In [66]:
print(presenter)
{'name': 'Mantas Zimnickas'}

Concurrent vs Parallel vs Asynchronous

  • Concurrency is about dealing with lots of things at once [1].
  • Possible ways to deal with concurrency:
    • Parallelisation.
    • Asynchronous IO.

Parallelisation

  • Parallelisation is about doing lots of things at once[1].
  • Parallelisation helps to solve CPU-bound performance issues.
  • Parallelisation tools in Python's standard library:
import multiprocessing
import threading
import concurrent.futures

Asynchronous IO

  • Asynchronous IO is about doing IO processing in a non-blocking way [2].
  • Asynchronous IO helps to solve IO bound performance issues.
  • Asynchornous IO tools in Python's standart library:
import asyncio

What is IO-bound?

Below is a list of possible reasons for IO-bound performance issues:

  • Reading/writing to a file.
  • Sending/receiving data over the network.
  • Executing database queries.
  • Executing processes and comunicating with them over pipes.
  • Waiting for user input from sys.stdin.
  • Writing output to sys.stdout or sys.stderr.

History of asynchronous IO in python

  • 2001 - PEP 255: Simple generators (yield statement)
  • 2002 - Twisted
  • 2009 - Gevent
  • 2010 - Tornado
  • 2010 - PEP 3153: Asynchronous IO support
  • 2012 - PEP 3156: Asynchronous IO Support Rebooted: the "asyncio" Module
  • 2014 - Tulip
  • 2014 - asyncio

Two types of asyncio users

  • Those who use high level asyncio coroutines.
  • Those who create new coroutines using low level asyncio primitives.

Callbacks

In [8]:
from functools import partial
from concurrent.futures import ThreadPoolExecutor

def add(a, b):
    return a + b

def done(a, b, future):
    print('%s + %s = %s' % (a, b, future.result()))
    
with ThreadPoolExecutor(max_workers=1) as pool:
    future = pool.submit(add, 2, 2)
    future.add_done_callback(partial(done, 2, 2))
2 + 2 = 4

Using coroutines

In [3]:
def add(a, b):
    return a + b

def main():
    print('2 + 2 =', add(2, 2))
    
main()
2 + 2 = 4
In [4]:
@asyncio.coroutine
def add(a, b):
    return a + b

@asyncio.coroutine
def main():
    print('2 + 2 =', (yield from add(2, 2)))

loop.run_until_complete(main())
2 + 2 = 4

Using coroutines: aiohttp

In [10]:
import aiohttp

@asyncio.coroutine
def get_size(url):
    response = yield from aiohttp.request('GET', url)
    return len((yield from response.read()))

@asyncio.coroutine
def main():
    size = yield from get_size('https://python.org/')
    print('python.org size is:', size)
    
loop.run_until_complete(main())
python.org size is: 46947
  • This example works much faster without asyncio.
  • asyncio shines with many concurent input/output.

Using coroutines: aiohttp

In [23]:
@asyncio.coroutine
def get_size(semaphore, url):
    with (yield from semaphore):
        response = yield from aiohttp.request('GET', url)
        return len((yield from response.read()))
 
@asyncio.coroutine
def main(urls):
    semaphore = asyncio.Semaphore(3)
    sizes = [get_size(semaphore, url) for url in urls]
    return [(yield from size) for size in asyncio.as_completed(sizes)]

urls = ['https://python.org/']
loop.run_until_complete(main(urls)) 
Out[23]:
[46947]

Using coroutines: aiohttp.web

In [5]:
from aiohttp import web

@asyncio.coroutine
def handle(request):
    return web.Response(body=b'Hello world.')

@asyncio.coroutine
def init(loop):
    app = web.Application(loop=loop)
    app.router.add_route('GET', '/', handle)
    return (yield from loop.create_server(app.make_handler(), '127.0.0.1', 8080))

loop.run_until_complete(init(loop))
Out[5]:
<asyncio.base_events.Server at 0x7f485c0ad198>

Generator

In [131]:
def generator():
    yield 1
    yield 2
    return 3
    
def wrapper():
    yield (yield from generator())

for x in wrapper():
    print(x)
1
2
3

Coroutine

  • In python a coroutine is same thing as generator.
  • Coroutine is a function with multiple entry points for suspending and resuming execution at certain locations [4].

Coroutine

In [123]:
def coroutine():
    print('b:', (yield 1))
    print('b:', (yield 3))
    print('b:', (yield 5))
In [124]:
a = coroutine()
In [125]:
print('a:', next(a))
a: 1
In [126]:
print('a:', a.send(2))
b: 2
a: 3
In [127]:
print('a:', a.send(4))
b: 4
a: 5

asyncio coroutine

In [17]:
@asyncio.coroutine
def my_coroutine():
    print('hello')
In [18]:
type(my_coroutine)
Out[18]:
function
In [19]:
type(my_coroutine())
Out[19]:
generator
In [21]:
asyncio.iscoroutinefunction(my_coroutine), asyncio.iscoroutine(my_coroutine())
Out[21]:
(True, True)

Future

  • A class used to represent a result that may not be available yet [3].
  • Also know as deffered from Twisted or promise in other libraries.
In [51]:
future = asyncio.futures.Future()
future.add_done_callback(lambda future: print("Future's done callback here."))
In [52]:
future.set_result("My result")
In [53]:
future.done()
Out[53]:
True
In [54]:
future.exception()
In [55]:
future.result()
Out[55]:
'My result'

Low level coroutines

In [29]:
busy, ready = 0, 1
responses = [busy, busy, ready]

def callback(future, n):
    if responses.pop(0) == ready:
        future.set_result(n)
    else:
        loop.call_later(1, callback, future, n+1)
    
def coroutine():
    future = asyncio.futures.Future()
    callback(future, 1)
    return future

loop.run_until_complete(coroutine())
Out[29]:
3

References