02-6. Multi-threading vs Multi-processing 完整對比

⏱️ 閱讀時間: 15 分鐘 🎯 難度: ⭐⭐ (簡單)


🎯 本篇重點

深入理解 Multi-threading(多執行緒)和 Multi-processing(多進程)的核心差異、適用場景及實戰案例。


🤔 核心差異:一句話總結

Multi-threading:一個程式內創建多個 Thread,共享記憶體空間。 Multi-processing:一個程式創建多個獨立 Process,各自擁有完全隔離的記憶體空間。


🏢 公司組織比喻

Multi-threading = 開放式辦公室

Yoru-Karu Studio(單一公司 = 1 個 Process)
│
├─ 共享辦公空間(共享記憶體)
│  ├─ 員工 A(Thread 1)→ 處理訂單
│  ├─ 員工 B(Thread 2)→ 接聽客服電話
│  ├─ 員工 C(Thread 3)→ 更新官網內容
│  └─ 員工 D(Thread 4)→ 回覆客戶郵件
│
└─ 共享資源
   ├─ 共用白板、檔案櫃(所有員工可直接存取)
   ├─ 溝通超快(面對面對話)
   ├─ 但可能搶資源(需要排隊協調)
   └─ 一人出錯可能影響全辦公室

特點:

  • ✅ 溝通極快(直接存取共享變數)
  • ✅ 資源共享(記憶體、檔案描述符)
  • ✅ 創建成本低
  • ⚠️ 需要同步機制(Lock、Semaphore)
  • ⚠️ 一個 Thread 崩潰可能影響整個 Process

Multi-processing = 獨立分公司

Yoru-Karu Group(母公司)
│
├─ 台北分公司(Process 1, PID: 1001)
│  ├─ 獨立辦公室(獨立記憶體空間)
│  ├─ 獨立資源(自己的檔案、資料庫連線)
│  └─ 員工 A, B, C
│
├─ 高雄分公司(Process 2, PID: 1002)
│  ├─ 獨立辦公室(獨立記憶體空間)
│  ├─ 獨立資源
│  └─ 員工 D, E, F
│
├─ 台中分公司(Process 3, PID: 1003)
│  └─ ...
│
└─ 溝通方式
   ├─ 電話、郵件、視訊會議(IPC: Pipe, Queue, Socket)
   ├─ 各自獨立運作
   ├─ 一間分公司倒閉不影響其他分公司
   └─ 溝通成本較高

特點:

  • ✅ 完全隔離(穩定性高)
  • ✅ 可利用多核 CPU 真正並行
  • ✅ 無 GIL 限制(Python)
  • ⚠️ 溝通成本高(需要 IPC)
  • ⚠️ 記憶體佔用大

💻 架構對比圖

Multi-threading 記憶體架構

Process (PID: 1000)
├─ 記憶體佈局
│  ├─ Code Segment (程式碼)        ← 所有 Thread 共享
│  ├─ Data Segment (全域變數)      ← 所有 Thread 共享
│  ├─ Heap (動態分配)              ← 所有 Thread 共享
│  └─ Stack
│     ├─ Thread 1 Stack (獨立)    ← 區域變數、函式呼叫
│     ├─ Thread 2 Stack (獨立)
│     ├─ Thread 3 Stack (獨立)
│     └─ Thread 4 Stack (獨立)
│
├─ 共享資源
│  ├─ 檔案描述符
│  ├─ 全域變數
│  └─ Heap 物件
│
└─ Thread
   ├─ Thread 1 (主 Thread)
   ├─ Thread 2
   ├─ Thread 3
   └─ Thread 4

特點:

  • 所有 Thread 可直接讀寫 Heap、Data Segment
  • 快速但需要同步

Multi-processing 記憶體架構

Master Process (PID: 1000)
└─ 創建並管理子 Process

Worker Process 1 (PID: 1001)
├─ 完全獨立的記憶體空間
│  ├─ Code Segment (獨立副本)
│  ├─ Data Segment (獨立副本)
│  ├─ Heap (獨立副本)
│  └─ Stack
│
└─ 獨立資源
   ├─ 獨立的檔案描述符
   ├─ 獨立的全域變數
   └─ 無法直接存取其他 Process

Worker Process 2 (PID: 1002)
├─ 完全獨立的記憶體空間
│  └─ ... (完全隔離)

Worker Process 3 (PID: 1003)
└─ ...

特點:

  • 每個 Process 有獨立記憶體
  • 需要 IPC 才能溝通

🔍 實戰案例對比

案例 1:創建與啟動

Multi-threading

from threading import Thread
import os
import time

def worker(name):
    print(f"[{name}] PID: {os.getpid()}, 開始工作")
    time.sleep(1)
    print(f"[{name}] 完成")

# 創建 3 個 Thread
threads = []
start = time.time()

for i in range(3):
    t = Thread(target=worker, args=(f'Thread-{i}',))
    t.start()
    threads.append(t)

for t in threads:
    t.join()

print(f"總耗時: {time.time() - start:.3f} 秒")

輸出:

[Thread-0] PID: 1000, 開始工作  ← 同一個 PID
[Thread-1] PID: 1000, 開始工作
[Thread-2] PID: 1000, 開始工作
[Thread-0] 完成
[Thread-1] 完成
[Thread-2] 完成
總耗時: 1.002 秒  ← 創建成本低

Multi-processing

from multiprocessing import Process
import os
import time

def worker(name):
    print(f"[{name}] PID: {os.getpid()}, 開始工作")
    time.sleep(1)
    print(f"[{name}] 完成")

# 創建 3 個 Process
if __name__ == '__main__':
    processes = []
    start = time.time()

    for i in range(3):
        p = Process(target=worker, args=(f'Process-{i}',))
        p.start()
        processes.append(p)

    for p in processes:
        p.join()

    print(f"總耗時: {time.time() - start:.3f} 秒")

輸出:

[Process-0] PID: 1001, 開始工作  ← 不同的 PID
[Process-1] PID: 1002, 開始工作
[Process-2] PID: 1003, 開始工作
[Process-0] 完成
[Process-1] 完成
[Process-2] 完成
總耗時: 1.05 秒  ← 創建成本稍高

案例 2:記憶體共享 vs 隔離

Multi-threading(共享記憶體)

from threading import Thread, Lock

# 共享變數(所有 Thread 可見)
shared_counter = {'value': 0}
lock = Lock()

def increment():
    for _ in range(100000):
        with lock:  # 需要 Lock 保護
            shared_counter['value'] += 1

# 兩個 Thread 同時修改共享變數
t1 = Thread(target=increment)
t2 = Thread(target=increment)

t1.start(); t2.start()
t1.join(); t2.join()

print(f"Counter: {shared_counter['value']}")
# 輸出:Counter: 200000 ← 正確(有 Lock)

沒有 Lock 的結果:

# 如果不用 Lock
shared_counter['value'] += 1  # 沒有 Lock

# 可能輸出:Counter: 150000 ← Race Condition!

Multi-processing(記憶體隔離)

from multiprocessing import Process

# 每個 Process 會得到獨立的變數副本
shared_counter = {'value': 0}

def increment():
    for _ in range(100000):
        shared_counter['value'] += 1
    print(f"Process {os.getpid()} counter: {shared_counter['value']}")

# 兩個 Process 各自修改自己的副本
p1 = Process(target=increment)
p2 = Process(target=increment)

p1.start(); p2.start()
p1.join(); p2.join()

print(f"Main Process counter: {shared_counter['value']}")

輸出:

Process 1001 counter: 100000  ← Process 1 的獨立副本
Process 1002 counter: 100000  ← Process 2 的獨立副本
Main Process counter: 0       ← 主 Process 的原始值(未被修改)

如果真的需要共享:

from multiprocessing import Process, Value, Lock

def increment(counter, lock):
    for _ in range(100000):
        with lock:
            counter.value += 1

if __name__ == '__main__':
    # 使用共享記憶體
    counter = Value('i', 0)  # 共享整數
    lock = Lock()

    p1 = Process(target=increment, args=(counter, lock))
    p2 = Process(target=increment, args=(counter, lock))

    p1.start(); p2.start()
    p1.join(); p2.join()

    print(f"Counter: {counter.value}")
    # 輸出:Counter: 200000

案例 3:CPU 密集型任務(關鍵差異!)

import time

def cpu_intensive_task():
    """CPU 密集運算"""
    total = 0
    for i in range(10000000):
        total += i * i
    return total

# 測試單執行
start = time.time()
cpu_intensive_task()
cpu_intensive_task()
cpu_intensive_task()
cpu_intensive_task()
print(f"單執行: {time.time() - start:.2f} 秒")

單執行輸出:

單執行: 10.0 秒

Multi-threading(受 GIL 限制)

from threading import Thread
import time

start = time.time()
threads = [Thread(target=cpu_intensive_task) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"Multi-threading: {time.time() - start:.2f} 秒")

輸出(4 核 CPU):

Multi-threading: 10.5 秒  ← 沒有加速!甚至更慢(GIL + Context Switch)

原因:

  • Python GIL(Global Interpreter Lock)限制
  • 同一時間只有一個 Thread 可以執行 Python 程式碼
  • 多個 Thread 輪流持有 GIL,無法真正並行

Multi-processing(真正並行)

from multiprocessing import Process, Pool
import time

# 方法 1:手動創建 Process
start = time.time()
processes = [Process(target=cpu_intensive_task) for _ in range(4)]
for p in processes:
    p.start()
for p in processes:
    p.join()
print(f"Multi-processing: {time.time() - start:.2f} 秒")

# 方法 2:使用 Process Pool(推薦)
start = time.time()
with Pool(processes=4) as pool:
    pool.map(lambda x: cpu_intensive_task(), range(4))
print(f"Process Pool: {time.time() - start:.2f} 秒")

輸出(4 核 CPU):

Multi-processing: 2.6 秒  ← 接近 4 倍加速!
Process Pool: 2.5 秒      ← Pool 更高效

原因:

  • 每個 Process 有獨立的 Python 解釋器
  • 每個 Process 有自己的 GIL
  • 4 個 Process 可在 4 個 CPU 核心上真正並行執行

案例 4:I/O 密集型任務

import time
import requests

def io_intensive_task(url):
    """I/O 密集任務:網路請求"""
    response = requests.get(url)
    return response.status_code

urls = ['https://httpbin.org/delay/1'] * 10

Multi-threading(推薦)

from threading import Thread

start = time.time()
threads = [Thread(target=io_intensive_task, args=(url,)) for url in urls]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"Multi-threading: {time.time() - start:.2f} 秒")

輸出:

Multi-threading: 1.2 秒  ← 快速!(10 個請求並發)

原因:

  • I/O 操作時會自動釋放 GIL
  • 其他 Thread 可以在 I/O 等待期間執行
  • 創建成本低

Multi-processing(不推薦)

from multiprocessing import Pool

start = time.time()
with Pool(processes=10) as pool:
    pool.map(io_intensive_task, urls)
print(f"Multi-processing: {time.time() - start:.2f} 秒")

輸出:

Multi-processing: 1.5 秒  ← 也快,但成本高

結論:

  • I/O 密集型用 Thread 即可
  • Process 創建成本高,沒必要

📊 完整對比表

基本特性對比

特性Multi-threadingMulti-processing
記憶體模型共享記憶體獨立記憶體
PID相同不同
創建速度快(微秒級)慢(毫秒級)
記憶體佔用小(幾 KB/Thread)大(幾 MB/Process)
溝通方式直接存取共享變數IPC(Pipe, Queue, Socket)
溝通速度極快
穩定性低(一個崩潰全崩潰)高(隔離)

Python 特定特性

特性Multi-threadingMulti-processing
GIL 影響✅ 受限制❌ 無影響
CPU 密集型❌ 無加速✅ 線性加速
I/O 密集型✅ 有加速✅ 有加速(成本高)
標準庫模組threadingmultiprocessing
最大數量幾千個(視系統)幾十個(CPU 核心數)

同步與安全

特性Multi-threadingMulti-processing
Race Condition✅ 容易發生❌ 不會發生(獨立記憶體)
需要 Lock✅ 是❌ 否(除非用共享記憶體)
Dead Lock 風險✅ 高❌ 低
除錯難度

🎯 選擇指南

選擇 Multi-threading 的場景

# ✅ I/O 密集型:網路、檔案、資料庫
from threading import Thread
import requests

def download_file(url):
    response = requests.get(url)  # I/O 操作時自動釋放 GIL
    return response.content

# 適合用 Thread
threads = [Thread(target=download_file, args=(url,)) for url in urls]

適用場景:

  • 🌐 網路爬蟲:大量 HTTP 請求
  • 📁 檔案 I/O:讀寫大量小檔案
  • 🗄️ 資料庫查詢:等待 SQL 查詢結果
  • 🔌 API 呼叫:呼叫第三方 API
  • 💬 聊天伺服器:等待訊息
  • 📧 郵件發送:SMTP I/O 等待

優點:

  • 創建快
  • 記憶體少
  • 溝通簡單

選擇 Multi-processing 的場景

# ✅ CPU 密集型:計算、資料處理
from multiprocessing import Pool

def process_data(data):
    # CPU 密集運算
    result = complex_computation(data)
    return result

# 適合用 Process
with Pool(processes=8) as pool:
    results = pool.map(process_data, large_dataset)

適用場景:

  • 🧮 科學計算:矩陣運算、模擬
  • 🖼️ 影像處理:濾鏡、轉檔
  • 🎬 影片處理:編碼、轉檔
  • 📊 大數據分析:資料清洗、統計
  • 🤖 機器學習:模型訓練
  • 🔐 密碼學:加密、解密
  • 🎮 遊戲物理引擎:碰撞檢測

優點:

  • 無 GIL 限制
  • 真正並行
  • 穩定性高

混合使用(Process + Thread)

from multiprocessing import Process
from threading import Thread
import requests

def worker_process(url_chunk):
    """每個 Process 內用多個 Thread"""
    def download(url):
        return requests.get(url).content

    # Process 內的 Thread
    threads = [Thread(target=download, args=(url,)) for url in url_chunk]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

# 將 URL 分配給多個 Process
url_chunks = [urls[i:i+10] for i in range(0, len(urls), 10)]
processes = [Process(target=worker_process, args=(chunk,)) for chunk in url_chunks]

for p in processes:
    p.start()
for p in processes:
    p.join()

案例:Gunicorn Web Server

Gunicorn Master Process
├─ Worker Process 1 (CPU core 1)
│  ├─ Thread 1 → 處理 HTTP 請求 A
│  ├─ Thread 2 → 處理 HTTP 請求 B
│  └─ Thread 3 → 處理 HTTP 請求 C
│
├─ Worker Process 2 (CPU core 2)
│  ├─ Thread 1 → 處理 HTTP 請求 D
│  └─ ...
│
└─ Worker Process 4 (CPU core 4)
   └─ ...

⚠️ 常見陷阱

陷阱 1:Thread 誤用於 CPU 密集型

# ❌ 錯誤:Thread 無法加速 CPU 運算
from threading import Thread

def cpu_task():
    return sum(i*i for i in range(10000000))

threads = [Thread(target=cpu_task) for _ in range(4)]
# GIL 限制,無加速

# ✅ 正確:用 Process
from multiprocessing import Pool

with Pool(4) as pool:
    results = pool.map(lambda x: sum(i*i for i in range(10000000)), range(4))

陷阱 2:Process 忘記 if name == ‘main

# ❌ 錯誤:Windows 上會無限創建 Process
from multiprocessing import Process

def worker():
    print("Working")

p = Process(target=worker)
p.start()

# ✅ 正確:加上保護
if __name__ == '__main__':
    p = Process(target=worker)
    p.start()
    p.join()

陷阱 3:創建過多 Process

# ❌ 錯誤:創建太多 Process 導致系統崩潰
for i in range(1000):
    Process(target=work).start()

# ✅ 正確:使用 Process Pool
from multiprocessing import Pool
import os

with Pool(processes=os.cpu_count()) as pool:
    pool.map(work, range(1000))

✅ 重點回顧

Multi-threading:

  • ✅ 共享記憶體,溝通極快
  • ✅ 創建成本低,適合大量 Thread
  • ✅ 適合 I/O 密集型任務
  • ❌ GIL 限制,無法加速 CPU 運算
  • ❌ 需要 Lock 防止 Race Condition

Multi-processing:

  • ✅ 獨立記憶體,穩定性高
  • ✅ 無 GIL,可真正並行
  • ✅ 適合 CPU 密集型任務
  • ❌ 創建成本高
  • ❌ 溝通需要 IPC,較慢

選擇原則:

  1. I/O 密集型 → Multi-threading
  2. CPU 密集型 → Multi-processing
  3. 混合型 → Process + Thread
  4. 大量並發 → Thread
  5. 需要隔離 → Process

上一篇: 02-5. Thread Pool 實戰 下一篇: 03-1. IPC 概述


最後更新:2025-01-06

0%