一個請求被誰處理

HTTP server 收到請求,需要有個執行單元去跑 handler。有兩種主流模型:

Thread-per-request(同步):每個請求分配一個 thread。Thread 在等 I/O(查資料庫、呼叫外部 API)時是阻塞的,那個 thread 什麼都不能做。

Event Loop(非同步):一個(或少數幾個)thread 管理所有請求。等 I/O 時,event loop 去處理其他請求,I/O 完成後回來繼續。

這個底層選擇,決定了框架的「實例模型」——同一個框架的 sync 和 async 版本,寫法、效能特性、適用場景都不一樣。


Python 的 WSGI vs ASGI

Python web 的並發模型分水嶺是 WSGI(同步)和 ASGI(非同步)。

WSGI(Web Server Gateway Interface):Django、Flask 的底層協議。每個請求是一個同步的函式呼叫:

# Django view — 同步函式
def user_detail(request, id):
    user = User.objects.get(pk=id)  # ← 阻塞,等資料庫
    return JsonResponse({'name': user.name})

Gunicorn 用多個 worker process 處理並發——每個 worker 同時只能跑一個請求。你要 100 QPS,就需要足夠多的 worker process,記憶體用量線性增長。

ASGI(Asynchronous Server Gateway Interface):FastAPI、Starlette 的底層協議。Handler 是 coroutine,等 I/O 時釋放 event loop:

# FastAPI view — 非同步函式
async def user_detail(id: int):
    user = await db.get(User, id)  # ← 非阻塞,await 期間處理其他請求
    return {'name': user.name}

Uvicorn 用 asyncio event loop,一個 process 可以同時等待數千個 I/O 操作。記憶體效率遠優於 WSGI 的 multi-process 模型。

Django 的 async 支援(Django 3.1+):Django 現在允許 async view,但 ORM 還是部分同步的——需要用 sync_to_async() 包裝,寫法比 FastAPI 繁瑣。這是框架設計歷史包袱的代價。


Node.js:天生 Event Loop

Node.js 的 Event Loop 是它的核心設計,Express 和所有 Node.js framework 都跑在上面:

  ┌─────────────────────────────────┐
  │           Event Loop            │
  │                                 │
  │  poll → check → timers → I/O   │
  │                                 │
  │  等 I/O 時,跑其他 callback      │
  └─────────────────────────────────┘
         Single Thread
// Express handler — 看起來同步,底層是 event loop
app.get('/users/:id', async (req, res) => {
  const user = await userRepo.findById(req.params.id); // ← await 讓出 event loop
  res.json(user);
});

Node.js 的單 thread event loop 在 I/O 密集場景(等資料庫、等 API)表現優秀,因為「等」的時候可以處理其他請求。

Node.js 的限制:CPU 密集任務(大量計算、圖片處理)會佔住 event loop,阻塞所有其他請求。解法是 Worker Threads 或把計算搬到 background job。


Java / Spring 的演化

Spring MVC(傳統):基於 Servlet,thread-per-request 模型,Tomcat 有 thread pool。

// Spring MVC — 同步,blocking
@GetMapping("/users/{id}")
public User show(@PathVariable Long id) {
    return userRepo.findById(id).orElseThrow(); // ← blocking,thread 等待
}

Spring WebFlux(reactive):基於 Netty + Project Reactor,non-blocking event loop 模型。

// Spring WebFlux — 非同步,non-blocking
@GetMapping("/users/{id}")
public Mono<User> show(@PathVariable Long id) {
    return userRepo.findById(id); // ← Mono = 非同步包裝
}

Kotlin + Coroutine + Ktor 讓這個寫法更自然:

// Ktor — Kotlin coroutine,寫起來像同步
get("/users/{id}") {
    val id = call.parameters["id"]!!.toLong()
    val user = userRepo.findById(id) // ← 底層是 coroutine,non-blocking
    call.respond(user)
}

為什麼同框架的 sync/async 寫法差很多

以 Django 為例:

# Django 同步 view
def user_list(request):
    users = User.objects.all()          # ← 同步 ORM
    return JsonResponse({'users': list(users.values())})
 
# Django 非同步 view(需要 async ORM)
async def user_list(request):
    users = await User.objects.all()   # ← 需要 Django 4.1+ 的 async ORM
    # 或用 sync_to_async
    users = await sync_to_async(list)(User.objects.all())
    return JsonResponse({'users': list(users)})

Django 的 ORM 從同步設計出發,async 是事後加上去的,所以有些操作必須用 sync_to_async() 包裝——這是歷史包袱。

FastAPI 從一開始就是 async-first,所以寫法很自然:

# FastAPI — 一致的 async 體驗
async def user_list(db: Session = Depends(get_db)):
    users = await db.execute(select(User))
    return users.scalars().all()

什麼時候選哪個模型

I/O 密集的 API(查資料庫、呼叫外部 API):async / event loop 模型更有效率。FastAPI、Node.js(Express / Hono)、Ktor、Spring WebFlux 在這個場景表現好。

CPU 密集的後端(大量計算、資料轉換):multi-thread / multi-process 更合適,因為 event loop 會被 CPU 佔住。Django(multi-worker)或 Java(thread pool)反而是對的選擇。

標準 CRUD API:老實說,除非你真的碰到效能瓶頸,不然框架的 sync vs async 差異對 95% 的 API 沒有感覺。真正的差異在高並發(1000+ QPS)場景才會出現。

混合場景:很多框架現在支援 sync/async 混用——FastAPI 可以宣告同步 handler(讓框架跑在 threadpool)、Django 可以有 async view。在遷移期間這很有用,但長期最好統一。


延伸閱讀