My story of

Async IO

Arduino

Socket Server

import socket


TCP_IP = '127.0.0.1'
TCP_PORT = 5005
BUFFER_SIZE = 2048  # Normally 1024, but we want fast response

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((TCP_IP, TCP_PORT))
s.listen(1)

while True:
    conn, addr = s.accept()
    print('Connection address:', addr)

    while 1:
        data = conn.recv(BUFFER_SIZE)
        if not data: break
        print("received data:", data)
        conn.send(data)  # echo

Because,

One client at a time

Let's add threads

import socket
import threading


TCP_IP = '127.0.0.1'
TCP_PORT = 5005
BUFFER_SIZE = 2048  # Normally 1024, but we want fast response

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((TCP_IP, TCP_PORT))
s.listen(1)


def connection(conn):
    data = conn.recv(BUFFER_SIZE)
    if not data: return
    print "received data:", data
    conn.send(data)  # echo


while True:
    conn, addr = s.accept()
    print 'Connection address:', addr

    while 1:
        t = threading.Thread(target=connection, args=(conn,))
        t.start()
        break

Because,

- Threads are overhead

- Context Switching

- Synchronization

- GIL

Still

Threadpool

import socket
from multiprocessing.pool import ThreadPool


TCP_IP = '127.0.0.1'
TCP_PORT = 5005
BUFFER_SIZE = 2048  # Normally 1024, but we want fast response

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((TCP_IP, TCP_PORT))
s.listen(1)


def connection(conn):
    data = conn.recv(BUFFER_SIZE)
    if not data: return
    print "received data:", data
    conn.send(data)  # echo

thread_pool = ThreadPool(5)

while True:
    conn, addr = s.accept()
    print 'Connection address:', addr

    while 1:
        thread_pool.map(
            connection, [conn]
        )
        break

Because,

- Threads are overhead

- Context Switching

- Synchronization

- GIL

Still

Scale sockets

Threadpool
processpool
GIL
processes
threads
os
pthreads

Event Loop

Node.js

When I heard about AsyncIO event loop

Hello world

import asyncio

async def say(what, when):
    await asyncio.sleep(when)
    print(what)

loop = asyncio.get_event_loop()
loop.run_until_complete(say('hello world', 1))
loop.close()
hello world

Output

Coroutine

Event loop

Hello World execution

import asyncio

async def say(what, when):
    await asyncio.sleep(when)
    print(what)

loop = asyncio.get_event_loop()
loop.run_until_complete(say('hello world', 1))
loop.close()

Hello world in Py 3.3

import asyncio


@asyncio.coroutine
def say(what, when):
    yield from asyncio.sleep(when)
    print(what)


loop = asyncio.get_event_loop()
loop.run_until_complete(say('hello world', 1))
loop.close()
hello world

Output

Coroutine

Event loop

def gen1():
    yield "a"
    yield "b"
    yield "c"


def gen2():
    for c in gen1():
        yield c

    yield "d"
    yield "e"
    yield "f"


print([i for i in gen2()])

Nested Yield

Yield from

def gen1():
    yield "a"
    yield "b"
    yield "c"


def gen2():
    yield from gen1()
    yield "d"
    yield "e"
    yield "f"
    return "RETURN VALUE"


def y_from():
    a = yield from gen2()
    print(a)


for i in y_from():
    print(i)

Output

a
b
c
d
e
f
RETURN VALUE
for c in gen1():
    yield c

Callbacks

import asyncio

def success(msg):
    print("Success - %s" % msg)

async def callback_example(loop):
    print('registering callbacks')
    loop.call_soon(success, "Call soon")

    loop.call_later(2, success, "Call later 2 sec")
    loop.call_later(3, success, "Call later 3 sec")

    loop.call_at(1506541660, success, "Network connect")
    loop.time()

    await asyncio.sleep(3)


loop = asyncio.get_event_loop()
loop.run_until_complete(callback_example(loop))
loop.close()
registering callbacks
Success - Call soon
Success - Call later 2 sec
Success - Call later 3 sec

Output

Callbacks with delay

import asyncio

def success(msg):
    print("Success - %s" % msg)

async def callback_example(loop):
    print('registering callbacks')
    loop.call_soon(success, "Call soon")

    loop.call_later(2, success, "Call later 2 sec")
    loop.call_later(3, success, "Call later 3 sec")

    loop.call_at(1506541660, success, "Network connect")
    loop.time()

    await asyncio.sleep(3)


loop = asyncio.get_event_loop()
loop.run_until_complete(callback_example(loop))
loop.close()
registering callbacks
Success - Call soon
Success - Call later 2 sec
Success - Call later 3 sec

Output

Execution callbacks

import asyncio

def success(msg):
    print("Success - %s" % msg)

async def callback_example(loop):
    print('registering callbacks')
    loop.call_soon(success, "Call soon")

    loop.call_later(2, success, "Call later 2 sec")
    loop.call_later(3, success, "Call later 3 sec")

    loop.call_at(1506541660, success, "Network connect")
    loop.time()

    await asyncio.sleep(3)


loop = asyncio.get_event_loop()
loop.run_until_complete(callback_example(loop))
loop.close()

Execution callbacks

import asyncio

def success(msg):
    print("Success - %s" % msg)

async def callback_example(loop):
    print('registering callbacks')
    loop.call_soon(success, "Call soon")

    loop.call_later(2, success, "Call later 2 sec")
    loop.call_later(3, success, "Call later 3 sec")

    loop.call_at(1506541660, success, "Network connect")
    loop.time()

    await asyncio.sleep(3)


loop = asyncio.get_event_loop()
loop.run_until_complete(callback_example(loop))
loop.close()

Future

import asyncio

async def slow_operation(future):
    await asyncio.sleep(1)
    future.set_result('Future is done!')

loop = asyncio.get_event_loop()
future = asyncio.Future()
asyncio.ensure_future(slow_operation(future))
loop.run_until_complete(future)
print(future.result())
loop.close()
Future is done!

Output

import asyncio


async def task_func():
    print('in task_func')
    return 'the result'


async def main(loop):
    print('creating task')
    task = loop.create_task(task_func())
    print('waiting for {!r}'.format(task))
    return_value = await task
    print('task completed {!r}'.format(task))
    print('return value: {!r}'.format(return_value))


event_loop = asyncio.get_event_loop()
try:
    event_loop.run_until_complete(main(event_loop))
finally:
    event_loop.close()
creating task
waiting for <Task pending coro=<task_func() running at tasks.py:4>>
in task_func
task completed <Task finished coro=<task_func() done, defined at tasks.py:4> result='the result'>
return value: 'the result'

Tasks

Executor

import asyncio
from concurrent.futures import ThreadPoolExecutor

print('running async test')


def say_boo():
    for i in range(10):
        print(i)


def say_baa():
    for i in range(10):
        print(i)


if __name__ == "__main__":
    executor = ThreadPoolExecutor(2)
    loop = asyncio.get_event_loop()
    boo = asyncio.ensure_future(loop.run_in_executor(executor, say_boo))
    baa = asyncio.ensure_future(loop.run_in_executor(executor, say_baa))

    loop.run_forever()

Debug

Debug logs can be enabled by
- Setting PYTHONASYNCIODEBUG=1​

- loop.set_debug(True)

logging.basicConfig(
    level=logging.DEBUG,
    format='%(levelname)7s: %(message)s',
    stream=sys.stderr,
)

Let's write echo
server in asyncio

import asyncio
import logging
import sys

SERVER_ADDRESS = ('localhost', 5555)
logging.basicConfig(
    level=logging.DEBUG,
    format='%(name)s: %(message)s',
    stream=sys.stderr,
)
log = logging.getLogger('main')


async def echo(reader, writer):
    address = writer.get_extra_info('peername')
    log = logging.getLogger('echo_{}_{}'.format(*address))
    log.debug('connection accepted')

    while True:
        data = await reader.read(128)
        if data:
            log.debug('received {!r}'.format(data))
            writer.write(data)
            await writer.drain()
            log.debug('sent {!r}'.format(data))
        else:
            log.debug('closing')
            writer.close()
            return

event_loop = asyncio.get_event_loop()
factory = asyncio.start_server(echo, *SERVER_ADDRESS)
server = event_loop.run_until_complete(factory)
log.debug('starting up on {} port {}'.format(*SERVER_ADDRESS))

try:
    event_loop.run_forever()
except KeyboardInterrupt:
    pass
finally:
    log.debug('closing server')
    server.close()
    event_loop.run_until_complete(server.wait_closed())
    log.debug('closing event loop')
    event_loop.close()

Streams API

TCP Echo server with callbacks

import asyncio
import logging
import sys

SERVER_ADDRESS = ('localhost', 5555)

logging.basicConfig(
    level=logging.DEBUG,
    format='%(name)s: %(message)s',
    stream=sys.stderr,
)
log = logging.getLogger('main')


class EchoServer(asyncio.Protocol):
    def connection_made(self, transport):
        self.transport = transport
        self.address = transport.get_extra_info('peername')
        self.log = logging.getLogger(
            'EchoServer_{}_{}'.format(*self.address)
        )
        self.log.debug('connection accepted')

    def data_received(self, data):
        self.log.debug('received {!r}'.format(data))
        self.transport.write(data)
        self.log.debug('sent {!r}'.format(data))


event_loop = asyncio.get_event_loop()
# Create the server and let the loop finish the coroutine before
# starting the real event loop.
factory = event_loop.create_server(EchoServer, *SERVER_ADDRESS)
server = event_loop.run_until_complete(factory)
log.debug('starting up on {} port {}'.format(*SERVER_ADDRESS))

# Enter the event loop permanently to handle all connections.
try:
    event_loop.run_forever()
finally:
    log.debug('closing server')
    server.close()
    event_loop.run_until_complete(server.wait_closed())
    log.debug('closing event loop')
    event_loop.close()

Synchronisation

  • Locks
  • Events
  • Condition
import asyncio
import functools


def unlock(lock):
    print('callback releasing lock')
    lock.release()


async def coro1(lock):
    print('coro1 waiting for the lock')
    with await lock:
        print('coro1 acquired lock')
    print('coro1 released lock')


async def coro2(lock):
    print('coro2 waiting for the lock')
    await lock
    try:
        print('coro2 acquired lock')
    finally:
        print('coro2 released lock')
        lock.release()


async def main(loop):
    lock = asyncio.Lock()
    print('acquiring the lock before starting coroutines')
    await lock.acquire()
    print('lock acquired: {}'.format(lock.locked()))
    loop.call_later(2, functools.partial(unlock, lock))
    print('waiting for coroutines')
    await asyncio.wait([coro1(lock), coro2(lock)]),


event_loop = asyncio.get_event_loop()
try:
    event_loop.run_until_complete(main(event_loop))
finally:
    event_loop.close()
acquiring the lock before starting coroutines
lock acquired: True
waiting for coroutines
coro1 waiting for the lock
coro2 waiting for the lock
callback releasing lock
coro1 acquired lock
coro1 released lock
coro2 acquired lock
coro2 released lock

Locks

Events

import asyncio
import functools


def set_event(event):
    print('setting event in callback')
    event.set()

async def coro1(event):
    print('coro1 waiting for event')
    await event.wait()
    print('coro1 triggered')

async def coro2(event):
    print('coro2 waiting for event')
    await event.wait()
    print('coro2 triggered')

async def main(loop):
    # Create a shared event
    event = asyncio.Event()
    print('event start state: {}'.format(event.is_set()))
    loop.call_later(
        2, functools.partial(set_event, event)
    )
    await asyncio.wait([coro1(event), coro2(event)])
    print('event end state: {}'.format(event.is_set()))

event_loop = asyncio.get_event_loop()
try:
    event_loop.run_until_complete(main(event_loop))
finally:
    event_loop.close()
event start state: False
coro1 waiting for event
coro2 waiting for event
setting event in callback
coro1 triggered
coro2 triggered
event end state: True

Condition

import asyncio

async def consumer(condition, n):
    with await condition:
        print('consumer {} is waiting'.format(n))
        await condition.wait()
        print('consumer {} triggered'.format(n))
    print('ending consumer {}'.format(n))


async def manipulate_condition(condition):
    print('starting manipulate_condition')

    await asyncio.sleep(0.1)

    for i in range(1, 3):
        with await condition:
            print('notifying {} consumers'.format(i))
            condition.notify(n=i)
        await asyncio.sleep(0.1)

    with await condition:
        print('notifying remaining consumers')
        condition.notify_all()

    print('ending manipulate_condition')

async def main(loop):
    # Create a condition
    condition = asyncio.Condition()

    consumers = [
        consumer(condition, i)
        for i in range(5)
    ]
    loop.create_task(manipulate_condition(condition))
    await asyncio.wait(consumers)

event_loop = asyncio.get_event_loop()
try:
    result = event_loop.run_until_complete(main(event_loop))
finally:
    event_loop.close()
starting manipulate_condition
consumer 4 is waiting
consumer 1 is waiting
consumer 0 is waiting
consumer 2 is waiting
consumer 3 is waiting
notifying 1 consumers
consumer 4 triggered
ending consumer 4
notifying 2 consumers
consumer 1 triggered
ending consumer 1
consumer 0 triggered
ending consumer 0
notifying remaining consumers
ending manipulate_condition
consumer 2 triggered
ending consumer 2
consumer 3 triggered
ending consumer 3

Queue

Some examples

Best practices

- Use Locks, Events and Condition

-  Avoid modification of global veriables

- Consider GIL

- Use async libraries

- Avoid recursions

Async IO

By Hitul Mistry

Async IO

  • 725