前言 / Introduction

在 Python 專案的開發過程,或多或少會碰到需要平行運行的時候,而大多的資料只說使用 asyncio 就可以加速,但實際上執行時,卻還是一樣的緩慢甚至更糟,那或許你也踩到了錯誤使用線程的坑。

在這篇文章,將討論下面幾點:

  • Why I/O-bound and CPU-bound tasks behave differently
  • The right way to use asyncio
  • Why requests.get() blocks async functions-

This sets the foundation for understanding Python 3.13 and 3.14 improvements later in the series.

I/O-Bound vs CPU-Bound: The Real Difference

在討論 asyncio or threads 之前,我們需要先搞清楚甚麼樣的任務類型是你面對的

TypeWhat slows it down?Best approach
I/O-boundWaiting on network, files, DB, APIasyncio, threads
CPU-boundCalculations, parsing, encodingmultiprocessing, free-threading
  • If your app is fetching APIs, reading files, or handling sockets → it’s I/O-bound.
  • If it’s crunching data or doing math → it’s CPU-bound.

本篇文章為了更清楚描述,接下來會專注在 I/O-bound 的討論。

Why Asyncio Exists (and Why Threads Are Not Enough)

雖然 Python 的 Thread 已經能夠很好的執行 I/O 任務,Interpreter 會在等待 I/O 的時候去釋放 GIL。但 asyncio 還可以更有效率的使用 single-threaded event loop,優化 Task 的執行。

For Example where asyncio shines:

  • HTTP calls
  • File operations
  • WebSockets & streaming
  • Database queries
  • Message queues

But asyncio only works if your code is non-blocking.

❌ The Classic Mistake: Using requests.get() in async def

有效率的使用,是在沒有踩到 asyncio 的坑,比較常見的錯誤案例,就是在 async def 中混用會造成 I/O blocking 的函式。

import asyncio
import requests

async def bad_fetch(url):
    resp = requests.get(url)  # ❌ This blocks the entire event loop
    return resp.text

async def main():
    await asyncio.gather(
        bad_fetch("https://httpbin.org/delay/3"),
        bad_fetch("https://httpbin.org/delay/3")
    )

asyncio.run(main())

即使已經使用 asyncio.gather(),但結果還是 sequential executionrequests.get() 會導致進程被 blocking, 所以 event loop 將會被鎖住,直到 request 完成。

✅ Correct Approach: Use an Async HTTP Client (aiohttp)

那避免踩坑的方式,就是正確使用相關的第三方 Library 。 以上面的例子,使用 aiohttp才是正確的方法。

import asyncio
import aiohttp

async def good_fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.text()

async def main():
    await asyncio.gather(
        good_fetch("https://httpbin.org/delay/3"),
        good_fetch("https://httpbin.org/delay/3")
    )

asyncio.run(main())
  • ✅ Now both requests happen concurrently
  • ✅ Event loop stays responsive
  • ✅ No threads needed

When to Choose Asyncio vs Threads for I/O

GoalUse This
Many HTTP callsaiohttp / httpx (async)
File I/Oaiofiles or threads
Legacy blocking coderun_in_executor()
Web serversFastAPI, aiohttp
Mixing CPU + I/Oasync + executors

Key Takeaways

  • Asyncio only works if your I/O calls are non-blocking
  • requests.get() blocks the event loop → use aiohttp or executor workaround
  • Standard library can do async sockets, but it’s low-level
  • Threads still work for I/O, but async scales better
  • This sets the stage for why free-threading and new interpreters matter later