# AsyncIO

Python’s [asyncio](https://docs.python.org/3/library/asyncio.html "Link to AsyncIO docs") library allows you to run single threaded "concurrent"
code using coroutines inside an event loop.

The event loop is designed for I/O over sockets and other resources,
it is especially good for working with client/server network connections.

Python >= 3.4 (best features and performance in 3.6).

This guide will assume you're on Python 3.7.

## Explanation

### Benefits

The event loop allows you to handle a larger number of network connections at once.

Non-blocking network IO, so you can have long running connections with very little performance
impact (HTML5 sockets for example).


### How web servers are typically designed

(Pyramid, Flask, Plone, etc)

- Processes X Threads = Total number of concurrent connections that can be handled at once.
- Client makes a request to web server, request is assigned thread, thread handle request and sends response
- If no threads available, request is blocked, waiting for an open thread
- Threads are expensive (CPU), Processes are expensive on RAM


### How it works with AsyncIO

- All requests are thrown on thread loop
- Since we don’t block on network traffic, we can juggle many requests at the same time
- Modern web application servers connect with many different services that can
  potentially block on network traffic — **BAD**
- Limiting factor is maxed out CPU, not costly thread switching between requests — **GOOD**


### Where is network traffic used?

- Web Client/App Server
- App Server/Database
- App Server/Caching(Redis)
- App Server/OAUTH
- App Server/Cloud storage
- App Server/APIs(Google Drive, Microsoft, Slack, etc)


### Implementation details

To benefit, the whole stack needs to be asyncio-aware.

Anywhere in your application server that is not and does network traffic
WILL BLOCK all other connections while it is doing its network traffic
(example: using the `requests` library instead of `aiohttp`)


## Basics

Get active event loop or create new one

Run coroutine inside event loop with `asyncio.run`


```python
import asyncio


async def hello():
    print('hi')


if __name__ == '__main__':
    asyncio.run(hello())
```

## Basics(2)

`asyncio.run` automatically wraps your coroutine into a Future
object and waits for it to finish.

`asyncio.create_task` will wrap a coroutine in a future and return it to you.

To demonstrate this you can schedule multiple coroutines that can run at the same time.

```python
import asyncio

async def hello1():
    print('before hi 1')
    await asyncio.sleep(0.5)
    print('hi 1')

async def hello2():
    print('hi 2')

async def run():
    future1 = asyncio.create_task(hello1())
    future2 = asyncio.create_task(hello2())
    await future1
    await future2

if __name__ == '__main__':
    asyncio.run(run())
```


## Long running tasks

You can also schedule long running tasks on the event loop.

The tasks can run forever…

“Task” objects are the same as “Future” objects (well, close)

```python
import asyncio
import random


async def hello_many():
    while True:
        number = random.randint(0, 3)
        await asyncio.sleep(number)
        print('Hello {}'.format(number))


async def run():
    task = asyncio.create_task(hello_many())
    print('task running now...')
    await asyncio.sleep(10)
    print('we waited 10 seconds')
    task.cancel()
    print('task cancelled')


if __name__ == '__main__':
    asyncio.run(run())
```


## ALL YOUR ASYNC BELONGS TO US

**gotcha**

If you want part of your code to be async (say a function), the complete stack of
the caller must be async and running on the event loop.


```python
import asyncio


async def print_foobar1():
    print('foobar1')


async def print_foobar2():
    print('foobar2')


async def foobar():
    await print_foobar1()
    print_foobar2()  # won't work, never awaited


async def run():
    await foobar()
    print_foobar1()  # won't work, never awaited

if __name__ == '__main__':
    asyncio.run(run())

# await print_foobar1()  # error, not running in event loop
```


## "multi" processing

AsyncIO isn't really multiprocessing but it gives you the illusion of it.

A simple example can be shown with the `asyncio.gather` function.

```python
import asyncio
import aiohttp


async def download_url(url):
    async with aiohttp.ClientSession() as session:
        resp = await session.get(url)
        text = await resp.text()
        print(f'Downloaded {url}, size {len(text)}')


async def run():
    await asyncio.gather(
        download_url('https://www.google.com'),
        download_url('https://www.facebook.com'),
        download_url('https://www.twitter.com'),
        download_url('https://www.stackoverflow.com')
    )


if __name__ == '__main__':
    asyncio.run(run())

```


## AsyncIO loops

Using `yield` with loops allows you to "give up" execution on every iteration of the loop.

```python
import asyncio


async def yielding():
    for idx in range(5):
        print(f'Before yield {idx}')
        yield idx


async def run():
    async for idx in yielding():
        print(f"Yay, I've been yield'd {idx}")


if __name__ == '__main__':
    asyncio.run(run())
```


## Scheduling


`loop.call_later`: arrange to call on a delay
`loop.call_at`: arrange function to be called at specified time


## Executors

An executor is available to use when you have non-async code that needs to be made async.

A typical executor is a thread executor. This means, anything you run in an executor
is being thrown in a thread to run.

It’s worse to have non-async code than to use thread executors.

Executors are also good for CPU bound code.

```python
import asyncio
import requests
import concurrent.futures


def download_url(url):
    resp = requests.get(url)
    text = resp.content
    print(f'Downloaded {url}, size {len(text)}')


async def foobar():
    print('foobar')


async def run():
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
    event_loop = asyncio.get_event_loop()
    await asyncio.gather(
        event_loop.run_in_executor(executor, download_url, 'https://www.google.com'),
        event_loop.run_in_executor(executor, download_url, 'https://www.facebook.com'),
        event_loop.run_in_executor(executor, download_url, 'https://www.twitter.com'),
        event_loop.run_in_executor(executor, download_url, 'https://www.stackoverflow.com'),
        foobar()
    )

if __name__ == '__main__':
    asyncio.run(run())

```

## Subprocess

Python also provides a very neat AsyncIO subprocess module.

```python
import asyncio


async def run_cmd(cmd):
    print(f'Executing: {" ".join(cmd)}')
    process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE)
    out, error = await process.communicate()
    print(out.decode('utf8'))


async def run():
    await asyncio.gather(
        run_cmd(['sleep', '1']),
        run_cmd(['echo', 'hello'])
    )


if __name__ == '__main__':
    asyncio.run(run())
```
