目錄

Multiprocessing, Multithreading and Asyncio in Python Part 1 - Basic Concept

Python 的效能瓶頸在幾年前一直為人詬病,
但在開發者的努力之下,Python 3.4 開始出現了 Asyncio 可以在特定情境下提升效能,
到了 Python 3.13 更出現了可選擇性關閉 GIL 的 Free-threaded (PEP-703) 設計,
結合過去的 Multiprocessing 和 Multithreading,
我整理了一下這三項技術適合的原理、差異和使用情境做了幾篇紀錄。
這一篇先簡單介紹三者的基本概念和適用情境。

Multiprocessing

一支程式可以同時執行多個獨立的行程 (Process)。
每個行程都有自己獨立的記憶體空間,
也因此可以完全避開 Python GIL (Global Interpreter Lock) 的限制。
這意味著不管是哪個版本,它們都可以真正地在多核心 CPU 上獨立的平行執行,
互不干擾。

適用情境:
CPU 密集型任務 (CPU-bound),
例如大量的數學運算、資料處理、影像辨識等。
可以有效利用多核心 CPU 的運算能力。

同時因為具有隔離性,
如果單一 process 崩潰也不會影響到同時正在運行的其他 process 或主程式。

import multiprocessing
import time

def cpu_bound_task(n):
    count = 0
    for i in range(n):
        count += i
    print(f"Finished task with {n}")

if __name__ == '__main__':
    start_time = time.time()
    processes = []
    for i in range(4):
        p = multiprocessing.Process(target=cpu_bound_task, args=(10**7,))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

    end_time = time.time()
    print(f"Multiprocessing took {end_time - start_time:.2f} seconds.")
  • 優點:
    • 能夠利用 multi-core CPU 實現真正的平行運算。
    • 不受 GIL 限制。
    • 行程間記憶體獨立,穩定性高,不太會產生 Race Condition。
  • 缺點:
    • 獨立 Process 建立所需的資源 (CPU, memory) 開銷較大。
    • 行程間通訊 (IPC) 較為複雜,需要透過 QueuePipe 或 share memory 等機制實現,且延遲較高。

Multithreading

在單一 Process 裡建立多個執行緒 (Thread)。
這些 thread 共享同一個 Process 使用的記憶體空間 (Heap),
因此可以輕鬆地進行資料共享與交換。

在 Python3.13 之前的版本,
Python 和其他程式語言如 C/C++ 等不同的是,
Python 的 multithreading 會受到 Python GIL (Global Interpreter Lock) 的限制,
即使是跑在多核心的 CPU 上,
Python 的 multithreading 實際上也做不到真正的平行運算。

GIL (Global Interpreter Lock): GIL 是 CPython (官方的 Python 實現) 中的一個機制, 為了保護 Python object (像是 dict, list 等)不會被損壞,
這個機制確保同一個時間點只有一個執行緒能執行 Python 的 bytecode。 這會導致在 CPU 密集型任務中, 即使在多核心 CPU 上, Python 的多執行緒也只能在一顆 core 上交替執行, 無法實現真正的平行運算達到加速的效果。

在過去仍有 GIL 的機制下,當 Thread 遇到 I/O 操作 (如讀寫檔案、網路請求) 時會釋放 GIL,
讓其他 Thread 有機會執行。
因此傳統上 Multithreading 主要被用於處理 I/O 密集型任務。

隨著 Python 的使用者越來越多,
產生了 PEP-703 這樣的需求,
直到 Python 3.13 之後開始實驗性的包含了可以選擇性關閉 GIL 的功能 (free-threading mode),
Python 3.14 開始出現了無 GIL 的 Free-threaded 的版本。
在無 GIL 的版本下,
Python 的 Multithreading 終於可以突破限制,
在多個核心同時平行處理 CPU-bound task,
避免 Multiprocessing 的 IPC 開銷。

適用情境:

  • Python 3.13 之前的版本:I/O 密集型任務 (I/O-bound),例如網路爬蟲、檔案下載、API 請求等。
  • Python 3.13+ 啟用 Free-threaded mode 之後百無禁忌。
import threading
import requests
import time

def io_bound_task(url):
    try:
        response = requests.get(url)
        print(f"Downloaded {url} with status {response.status_code}")
    except Exception as e:
        print(f"Error downloading {url}: {e}")

if __name__ == '__main__':
    urls = ["https://www.google.com"] * 5
    start_time = time.time()
    threads = []
    for url in urls:
        t = threading.Thread(target=io_bound_task, args=(url,))
        threads.append(t)
        t.start()

    for t in threads:
        t.join()

    end_time = time.time()
    print(f"Multithreading took {end_time - start_time:.2f} seconds.")
  • 優點:
    • 建立 thread 的開銷比 process 小。
    • 共享記憶體,資料交換方便。
  • 缺點:
    • Python 3.13 前的版本受 GIL 限制,無法利用多核心 CPU 處理 CPU 密集型任務。
    • 需要處理執行緒同步問題,如使用 Lock 來避免 Race condition。

Asyncio I/O (Asyncio) 與 Coroutine

非同步是 Python 3.4 之後引入的標準函式庫,
概念上是利用 Event Loop 和 Coroutine 來實現 single thread 下的並行(Concurrency)。

Coroutine 可以看作是一種輕量級的 thread,
可以被控制執行到某個需要等待 IO 的地方(await)時暫停,
等待需要被執行的任務完成,
在等待的同時將控制權交還給 event loop 去執行其他 coroutine。

當暫停的條件完成後 (例如等待的 I/O 操作完成),
event loop 會再回來繼續執行該 coroutine。

除了可以在暫停時間執行其他 coroutine 之外,
也可以節省作業系統層級的執行緒切換 (context switch),
可以提升不少效能。

適用情境:
高度並行的 I/O 密集型任務,
特別是需要同時處理大量網路連線的場景 (如 Web 伺服器、聊天應用、巨量 API requests)。

import asyncio
import aiohttp
import time

async def async_io_bound_task(session, url):
    try:
        async with session.get(url) as response:
            print(f"Downloaded {url} with status {response.status}")
    except Exception as e:
        print(f"Error downloading {url}: {e}")

async def main():
    urls = ["https://www.google.com"] * 5
    start_time = time.time()
    async with aiohttp.ClientSession() as session:
        tasks = [async_io_bound_task(session, url) for url in urls]
        await asyncio.gather(*tasks)
    end_time = time.time()
    print(f"Asyncio took {end_time - start_time:.2f} seconds.")

if __name__ == '__main__':
    asyncio.run(main())
  • 優點:
    • Context switch 的成本極低,能夠以極高的效率處理大量 I/O 操作。
    • 在 single thread 下運作,沒有 OS 層級的 race condition 的問題。(但在應用層 race condition 還是有可能被使用者自己寫出來的,自己要注意)
  • 缺點:
    • 不適用於 CPU 密集型任務。CPU-bound task 一個就可以卡住整個 Event loop。
    • 需要使用 async/await 語法,且需要有對應的非同步函式庫支援 (如 aiohttp, asyncpg)。

比較總結

特性多程序 (Multiprocessing)多執行緒 (Multithreading)非同步 (Asyncio)
基本單位行程 (Process)執行緒 (Thread)協程 (Coroutine)
記憶體空間獨立共享共享 (單執行緒)
GIL 影響無,直接繞過舊版受限制,3.13+ 可以避開無 (單執行緒)
平行/並行平行 (Parallelism)舊版並行 (Concurrency),3.13+ 平行 (Parallelism)並行 (Concurrency)
適用情境CPU 密集型、高容錯隔離一般 I/O 密集型海量/高度並行的 I/O 密集型
優點可利用多核心、穩定性高共享記憶體、開銷低極高 I/O 吞吐量、低開銷
缺點資源開銷大、IPC 複雜舊版(3.13 前)受 GIL 限制、有競爭條件需要處理複雜的 Lock不適用 CPU 密集型任務 (Event Loop 會卡死)
  • 如果是 CPU-bound 的任務需求,需要大量 CPU 運算,那麼不論新舊版的 Python multiprocessing 都能處理,它能充分利用多核心 CPU。Python 3.13+ 的 multithreading 也可以,還能降低 context switch 的開銷及 IPC 的複雜度。
  • 如果是 IO-bound 的任務需求,且邏輯相對簡單、連線數不大,multithreading 是一個寫法簡單且輕量的選擇。
  • 如果是 IO-bound 且需要處理大量的並行連線 (例如開發 Web 伺服器或 API、微服務),那麼 asyncio 是效能與吞吐量最高的。