Introduction:

Async IO is a concurrent programming design that has received dedicated support in Python, evolving rapidly from Python 3.4 through 3.7, and probably beyond. In the one sentence asyncio is a package to write concurrent code using the async/await syntax.
  • async is used to define a coroutine (an async function)
  • await is used to pause execution of a coroutine until an awaited result is ready.
coroutine (an async function)
  • it is essentially a function that can pause and resume execution. When you call a coroutine, it doesn't immediately run; it returns a coroutine object.
To execute coroutine, you need to:
  • Use await if you're inside an async function.
  • Use an event loop like asyncio.run() if you're in a synchronous context.

Note:

- the term async IO to denote the language-agnostic design of asynchronous IO
- while asyncio refers to Python package

View of Async IO in Concurrency world:


Where Does Async IO Fit In?

  • Parallelism consists of performing multiple operations at the same time.
  • Multiprocessing is a means to effect parallelism, and it entails spreading tasks over a computer’s central processing units (CPUs, or cores). Multiprocessing is well-suited for CPU-bound tasks: tightly bound for loops and mathematical computations usually fall into this category.
  • Concurrency is a slightly broader term than parallelism. It suggests that multiple tasks have the ability to run in an overlapping manner. (There’s a saying that concurrency does not imply parallelism.)
  • Threading is a concurrent execution model whereby multiple threads take turns executing tasks. One process can contain multiple threads. Python has a complicated relationship with threading thanks to its GIL. What’s important to know about threading is that it’s better for IO-bound tasks. While a CPU-bound task is characterized by the computer’s cores continually working hard from start to finish, an IO-bound job is dominated by a lot of waiting on input/output to complete.
    • To recap the above, concurrency encompasses both multiprocessing (ideal for CPU-bound tasks) and threading (suited for IO-bound tasks). Multiprocessing is a form of parallelism, with parallelism being a specific type (subset) of concurrency. The Python standard library has offered longstanding support for both of these through its multiprocessing, threading, and concurrent.futures packages.

  • Other concept, asynchronous IO enabled through the standard library’s asyncio package and the new async and await language keywords:
    • The asyncio package is billed by the Python documentation as a library to write concurrent code. However, async IO is not threading, nor is it multiprocessing. It is not built on top of either of these.
    • In fact, async IO is a single-threaded, single-process design: it uses cooperative multitasking, a term that you’ll flesh out by the end of this tutorial. It has been said in other words that async IO gives a feeling of concurrency despite using a single thread in a single process. Coroutines (a central feature of async IO) can be scheduled concurrently, but they are not inherently concurrent.
    • To reiterate, async IO is a style of concurrent programming, but it is not parallelism. It’s more closely aligned with threading than with multiprocessing but is very much distinct from both of these and is a standalone member in concurrency’s bag of tricks.
    • That leaves one more term. What does it mean for something to be asynchronous? This isn’t a rigorous definition, but for our purposes here, I can think of two properties:
      • Asynchronous routines are able to “pause” while waiting on their ultimate result and let other routines run in the meantime.
      • Asynchronous code, through the mechanism above, facilitates concurrent execution. To put it differently, asynchronous code gives the look and feel of concurrency.
Async IO Explained
  • Cooperative multitasking is a fancy way of saying that a program’s event loop communicates with multiple tasks to let each take turns running at the optimal time.
Async IO Is Not Easy
  • Python’s async model is built around concepts such as callbacks, events, transports, protocols, and futures.

Questions


1a. What is Event Loop and how is created?
  • Main and central mechanism of asyncio package. It is responsible for managing coroutines execution and handling event in asynchronous application.
  • Event loop allows to halt and start again coroutines. Because of this many tasks can be executed simultaneously.
  • Commonly Event loop is created as below when it creates Event loop and trigger coroutine.
  • asyncio.run(coroutine_function())
  • More complex way:
  • loop = asyncio.get_event_loop()
    loop.run_until_complete(coroutine_function())

1b. How to define asynchronous function in Python?
    async def fetch_data():
        data = await some_network_request()
        return data
  • How to trigger async function?
    • # from other coroutine function:
      async def main():
          result = await coroutine_funct()
          print(result)
      
      # from synchronous context:
      asyncio.run(coroutine_funct())
                          
2a. What is basic task for await keyword in asyncio?
  • It halts execution of current coroutine and allows other Event loop tasks to be executed. Because of this all program is not blocked.
  • await also waits for finishing execution of awaitable object (other async function)
  • await works only in async context in function defined by async. When used in synchronous context then SyntaxError is raised.
2b. What happen if you forget to use await keyword when calling async function
  • Coroutine object will be created but not executed.
  • import asyncio
    
    async def fetch_data():
        print("Fetching data...")
        await asyncio.sleep(2)
        print("Data fetched!")
    
    async def main():
        fetch_data()  # Brak await
        print("This will print immediately")
    
    asyncio.run(main())
  • fetch_data() returns coroutine object, but will not be executed
  • Also, RuntimeWarning will be triggered (starting from Python 3.8)
  • RuntimeWarning: coroutine 'fetch_data' was never awaited
3a. How to create and start task in asyncio
  • Task it is object of class: '_asyncio.Task' created by asyncio.create_task(). It manages coroutine execution.
  • When task is created then automatically coroutine is started in background.
  • 
    async def my_coroutine():
        print("Start")
        await asyncio.sleep(1)
        print("End")
    
    async def main():
        task = asyncio.create_task(my_coroutine())
        print("Task created")
        await task
    
    asyncio.run(main())
    
  • Differences between task and coroutine
  • Feature Coroutine Task
    Trigerring Not automatically Coroutine is triggered immediately in background.
    Parallelism Executes in the context of the current await / Event loop It is tool allowing coroutine to be executed parallel.
    State Lack of self-monitoring capabilities Task has self-monitoring features.
2b. What is difference beetwen asyncio.run and loop.run_until_complete?
asyncio.run()
  • Create new Event loop.
  • Executes the given coroutine.
  • When coroutine is finished it closes Event loop and clean resources.
  • Event loop is automatically managed.
  • Easy to use, but can be used only only once in the program.
loop.run_until_complete()
  • More complex to use.
3a. How to create and run more tasks in parallel?
  • asyncio.gather()
  • asyncio.create_task() => should be run in some loop because only 1 task can be created by call
3b. What is difference between 'asyncio.gather()' and 'asyncio.create_task()'?
    import asyncio

    async def task1():
        await asyncio.sleep(1)
        print("Task 1 done")

    async def task2():
        await asyncio.sleep(2)
        print("Task 2 done")

    async def task3():
        await asyncio.sleep(3)
        print("Task 3 done")

    async def main():
        # Uruchamiamy wszystkie zadania jednocześnie i czekamy na ich zakończenie
        await asyncio.gather(task1(), task2(), task3())

    # Uruchomienie event loop
    asyncio.run(main())
  • await asyncio.gather() - multiple task run in one call
  • asyncio.create_task() => it creates spacial objects (Tasks) that start coroutine immediately. It should be run in some loop because only 1 task can be created by call