For I/O-bound tasks, majority of the time is spent waiting - you might send off a 1KB request and it'll take 400ms for the server to receive, process, and send back a response, during which your program isn't doing anything else. Synchronous functions usually wait out that time, or in other words "block", so they're able to return that response to you: ```py response = requests.get(...) # ^^^^^^^^^^^^^^^^^ # Function blocks for response print(response.content)``` But if you've heard of non-blocking sockets, that is the operating system providing a way to send/receive data over your network *without* blocking. `asyncio` combines those APIs with async/await coroutines (functions that can stop and resume) to let you resume some other functions in the same thread, until it gets back a response and it can resume the original function that made the request: ```py async def main(): async with asyncio.TaskGroup() as tg: task1 = tg.create_task(func1()) task2 = tg.create_task(func2()) async def func1(): async with httpx.AsyncClient() as client: response = await client.get(...) # ^^^^^^^^^^^^^^^^^^^^^ # Coroutine yields to event loop async def func2(): # Can run while func1() has yielded for i in range(10): print("func2 at:", i) await asyncio.sleep(1) ``` (see also ) In contrast, CPU-bound tasks like image processing require your thread to run a bunch of instructions. In these cases, asyncio can't complete them any faster than a regular, synchronous program. To make it faster on a multi-core CPU, that workload would need to be shared across multiple threads (or processes) so the CPU can execute those instructions in parallel.
Are you available for vc?