前言 / 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 之前,我們需要先搞清楚甚麼樣的任務類型是你面對的
| Type | What slows it down? | Best approach |
|---|---|---|
| I/O-bound | Waiting on network, files, DB, API | asyncio, threads |
| CPU-bound | Calculations, parsing, encoding | multiprocessing, 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 execution。 requests.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
| Goal | Use This |
|---|---|
| Many HTTP calls | aiohttp / httpx (async) |
| File I/O | aiofiles or threads |
| Legacy blocking code | run_in_executor() |
| Web servers | FastAPI, aiohttp |
| Mixing CPU + I/O | async + executors |
Key Takeaways
- Asyncio only works if your I/O calls are non-blocking
requests.get()blocks the event loop → useaiohttpor 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