Skip to main content

Python

Async programming

  • Single thread, concurrent execution, it uses cooperative multitasking.
  • Coroutines are functions that can be paused and resumed.
    • Coroutines are repurposed generators that take advantage of the peculiarities of generator methods.
    • A coroutine is just the very fancy term for the thing returned by an async def function.
    • Old @asyncio.coroutine also exist.
  • Event loop is the main loop that drives the execution of coroutines.
    • You can think of an event loop as something like a while True loop that monitors coroutines, taking feedback on what’s idle, and looking around for things that can be executed in the meantime. It is able to wake up an idle coroutine when whatever that coroutine is waiting on becomes available.
  • Tasks are used to schedule coroutines concurrently.
  • TaskGroup combines Tasks cleanly.
  • asyncio.sleep() is used to stand in for a non-blocking call (but one that also takes some time to complete)
  • A few methods to work it out
    1. Method 1: Nonblocking but total time 3 seconds cz they are not started simultaneously.
    2. Method 2: Two seconds run using Task.
    3. Method 3: Use TaskGroups to implicitly call await.
    4. Method 4: asyncio.gather. Gather waits until all the coroutines are completed. However, use asyncio.as_completed() to get tasks as they are completed, in the order of completion.
import time
import asyncio

async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)

async def main():
print(f"started at {time.strftime('%X')}")
await say_after(1, 'hello')
await say_after(2, 'world')
print(f"finished at {time.strftime('%X')}")

asyncio.run(main())
  • Some AIO libs: aiohttp aiofiles aiodns

  • Async IO Design Patterns

    1. Chaining Coroutines
    2. Using a Queue
import time
import asyncio
import random

async def step1(n):
t = random.randint(0, 10)
print(f" Step 1: {n} waiting {t} seconds")
await asyncio.sleep(t)
result = f"result{n}-1"
# print(f" Step 1: {n} | Returning ==> {result}.")
return result

async def step2(n, arg):
t = random.randint(0, 10)
print(f" Step 2: {n} waiting {t} seconds")
await asyncio.sleep(t)
result = f"result{n}-2 derived from {arg}"
# print(f" Step 2: {n} | Returning == {result}.")
return result

async def chain(n):
start = time.perf_counter()
value1 = await step1(n)
value2 = await step2(n, value1)
end = time.perf_counter()
print(f"> Chained result {n} took {end - start} seconds.")
return value2

async def main():
start = time.perf_counter()
values = await asyncio.gather(*[chain(n) for n in [3, 6, 9]])
end = time.perf_counter()
print("-------")
print(f"Total time: {end - start} seconds")
print(values)

asyncio.run(main())

Neither asynchronous generators nor comprehensions make the iteration concurrent.

async def mygen(u: int = 10):
"""Yield powers of 2."""
i = 0
while i < u:
yield 2 ** i
i += 1
await asyncio.sleep(0.1)

async def main():
# This does *not* introduce concurrent execution
# It is meant to show syntax only
g = [i async for i in mygen()]
f = [j async for j in mygen() if not (j // 3 % 5)]
return g, f

References

  1. Python Async documentation, Coroutines
  2. Async Python
  3. FastAPI Async

Testing

Types of Testing

  1. Unit Testing
  2. Integration Testing
  3. System Testing
  4. Acceptance Testing

References

  1. Made with ML testing
  2. Pytest CI
  3. Fast API
  4. Fast API file structure
  5. fastapi pytest github template.