# Concurrency和Parallelism是什麼? > 最近因為面試被問到這個問題,想說這蠻值得拿出來討論的,因次這篇會探討一下這兩者的差異還有適用情境 大家可以去看看ByteByteGo的這部影片,講解的很好! {%preview https://youtu.be/RlM9AfWf1WU?si=I16McviLzHQe1i-B %} 附圖是影片中的截圖👇  簡單來說, - **Concurrency**:是指多個任務在單一核心中交錯執行,看起來像同時進行,實際上**快速切換處理**。 - **Parallelism**:則是指多個任務在多核心上**真正同時執行**,可顯著提升效能。 打個比方,Concurrency 是一位廚師交替煮湯與煎蛋,Parallelism 則是有兩位以上廚師各煮一道菜,來提升效率。 # 任務形態有什麼呢? 那再來就是要介紹說任務形態有什麼了,我們需要先知道我們的任務情境才知道如何搭配Concurrency或Parallelism喔 - **I/O-bound(I/O 密集)**:主要時間花在「等待外部資源」:如硬碟、網路、資料庫等。CPU 在等待期間「閒置」。 - **CPU-bound(CPU 密集)**:任務需要大量「運算處理」,CPU 長時間滿載運作,例如影像編碼、加密、科學計算等。 > [!Note] > 當然現實中的任務很多都是混合的喔,我們可以判斷現實中的任務哪種任務比例更高喔! # Python的技術與其對應情境 | 技術名稱 | 適合任務型態 | 是否多核心 | 是否能真正同時做 | 備註 | | -------- | -------- | -------- | -------- | -------- | | threading | I/O-bound | 否 | ❌ GIL限制 | 可交錯處理I/O任務 | | multiprocessing | CPU-bound | 是 | ✅ 真正並行 | 每個進程有獨立記憶體 | # 任務以及其Python技術對照範例 ## I/O-bound 範例(適合 threading) ```python= import threading import time def download_file(name): print(f"{name} 開始下載...") time.sleep(2) # 模擬等待下載 print(f"{name} 下載完成") start = time.time() t1 = threading.Thread(target=download_file, args=("檔案1",)) t2 = threading.Thread(target=download_file, args=("檔案2",)) t1.start(); t2.start() t1.join(); t2.join() end = time.time() print(f"總共耗時:{end - start:.2f} 秒") ``` Result  這邊是示意圖解釋說此範例發生了什麼事 ``` 時間軸 → → → → → → → → → → 任務1: ███░░░███░░░███ (執行 -> 等待 -> 執行 -> 等待 -> 執行) 任務2: ███░░░███░░░███ (稍後開始,也是交錯等待) 總耗時: 約 2 秒(兩任務重疊等待,不需等待兩次) ``` ## CPU-bound 範例(適合 multiprocessing) ```python= from multiprocessing import Process import time def heavy_calculation(name): print(f"{name} 開始計算...") total = sum(range(10**7)) print(f"{name} 計算完成,總和為 {total}") if __name__ == "__main__": start = time.time() p1 = Process(target=heavy_calculation, args=("任務1",)) p2 = Process(target=heavy_calculation, args=("任務2",)) p1.start() p2.start() p1.join() p2.join() end = time.time() print(f"總共耗時:{end - start:.2f} 秒") ``` Result  這邊是示意圖解釋說此範例發生了什麼事 ``` 時間軸 → → → → → → → → → 任務1: █████████████████ (密集 CPU 計算不中斷) 任務2: █████████████████ (另一顆核心同時運行) 總耗時: 約 ? 秒(依照實際程式碼計算程度而定) ``` ## 如果我們使用 threading 處理 CPU-bound 任務呢? **"這些 Thread會「輪流使用同一核心」,變成交錯執行,增加切換開銷。"** 雖然開了多個 Thread,但 GIL(Global Interpreter Lock)只允許一個 Thread 在任意時間點執行 Python bytecode。 > [!Note] > ### 為什麼 Python 需要 GIL? > GIL(Global Interpreter Lock,全域直譯器鎖)是 CPython(Python 最主流的實作)為了記憶體安全性與簡化 Thread 模型而設計的一把「全域鎖」。 > #### 簡化記憶體管理 > - CPython 使用 reference counting 來追蹤每個物件的使用者數量。 > - 若多個 Thread 同時對同一物件進行增減 reference count,就會產生 race condition。 > - 為了解決這問題,最簡單的做法就是鎖住整個直譯器 ➜ GIL。 ## 如果使用 multiprocessing 處理 I/O-bound 任務呢? - 技術上可行,但非最佳解 - 每個 Process 都是獨立的 Python 虛擬機,確實能交錯等待 I/O,不會被 GIL 限制。 - 但建立 Process 的成本比 Thread 高非常多(啟動、記憶體分離、序列化等),對 I/O 任務來說是資源浪費。 # Asyncio又是什麼呢? asyncio 是 Python 處理 Concurrency(尤其是 I/O-bound 任務)的核心模組之一,適合用來比較 threading 的替代方案: ## 範例 ``` import asyncio async def download_file(name): print(f"{name} 開始下載...") await asyncio.sleep(2) # 非同步等待 print(f"{name} 下載完成") async def main(): await asyncio.gather( download_file("檔案1"), download_file("檔案2") ) asyncio.run(main()) ``` Result  這邊是示意圖解釋說此範例發生了什麼事 ``` 時間軸 → → → → → → → → → → 任務1: ███░░░░░░░░░░███ (開始後馬上 await,直到結束) 任務2: ███░░░░░░░░░░███ (幾乎同時開始,也 await,直到結束) 總耗時: 約 2 秒 ``` - asyncio 協程之間靠 event loop 切換控制權 - 這兩個任務幾乎完全同時開始與結束,但 asyncio 實際上做的事情是「你等的時候我先去做別的」,也就是同時處理很多等待(I/O-bound)任務,但不是同時執行 CPU-bound 任務。 # 結論 | 任務特性 | 建議技術 | 原因 | |------------------|---------------------------|--------------------------------------------------| | I/O-bound | `threading` 或 `asyncio` | 能有效利用等待時間處理其他任務 | | 大量 I/O 且輕量任務 | `asyncio` | 無須多執行緒切換,效率最佳 | | CPU-bound | `multiprocessing` | 避開 GIL 限制,真正多核心運算 | | 混合型任務 | 視比例混合使用 | 可以搭配 `concurrent.futures` 動態選擇策略 |
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up