Introduction

AysncIO in python has two keywords: async/await. Many people who first encounter async concept might wonder isn’t that python can only have one thread in execution given the constraint of GIL?

Indeed, ayncio is bound by GIL and it can’t run more than one task at any moment as is shown below. This means that if another thread needs to run, the ownership of the GIL must be passed from the current executing thread to the other thread. This is what is called preemptive concurrency. This kind of switching is expensive when there are lots of threads.

The core concept in asyncio is coroutine. asyncio has its own concurrency synchronization through coroutine. It coordinates task switch with little cost. Simply put, python emulate concurrency in one thread through coroutine using event loop.

Thread and coroutine Thread and coroutine

Coroutine style synchronization still has its overhead, why we would bother switching tasks? The reason is behind the io part in asyncio. Think about that you have the following three tasks:

  • Task1: cooking rice takes 40 mins
  • Task2: washing clothes takes 30 mins
  • Task3: dish washing takes 30 mins

How much it would take a person to complete all these tasks. It won’t take us 100 mins for all these tasks because we just need to kick things off and have machines done for us. On the contrary, the following tasks most likely will consume us 100 mins because we have to get involved attentively.

  • Task1: watching tv 30 mins
  • Task2: jogging 30 mins
  • Task3: playing video games 40 mins

This example is just how illustrate where async ops help in Python – only in IO-bound programs such as http requests, file I/O etc, but not in CPU-bound programs. Note that in reality, python won’t allow us to coordinate the execution of each tasks. We can only pack tasks and send them for async execution.

import asyncio
import time


async def async_task():
    now = time.time()
    await asyncio.sleep(1)
    print("Doing async tasks")
    await asyncio.sleep(1)
    print(time.time() - now)


def sync_task():
    now = time.time()
    time.sleep(1)
    print("Doing async tasks")
    time.sleep(1)
    print(time.time() - now)


async def main():
    await asyncio.gather(*[async_task() for _ in range(3)])

now = time.time()
# run 3 async_task() coroutine concurrently
asyncio.run(main())
print(f"Time elapsed for running 3 coroutine tasks: {time.time() - now}")


now = time.time()
# run 3 sync_task() coroutine concurrently
sync_task()
sync_task()
sync_task()
print(f"Time elapsed for running 3 sync tasks: {time.time() - now}")