Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Context Switch 完整比較:OS 底層 vs 語言層級

整理日期:2026-02-24
涵蓋:作業系統原理、C/C++、Rust、Go


一、什麼是 Context Switch(上下文切換)?

Context Switch 是指 CPU 停止執行當前任務,並切換去執行另一個任務 的過程。

切換時需要儲存的狀態(CPU Context)

類別內容
通用寄存器RAX, RBX, RCX, RDX …
程序計數器RIP(下一條要執行的指令位置)
Stack PointerRSP(目前 stack 頂端)
CPU FlagsRFLAGS(條件旗標)
FPU / SIMD 狀態XMM / YMM 寄存器(浮點數與向量運算)
記憶體映射CR3(Page Table 基址,process 切換時需要換)

Process 切換Thread 切換 更貴,因為需要額外更換 Page Table(TLB flush)。


二、Context Switch 的層次分類

┌─────────────────────────────────────────────────┐
│               應用程式層(Developer)              │
│  Go context.Context / Rust Future / C++ coroutine │
├─────────────────────────────────────────────────┤
│               語言執行期層(Runtime)              │
│  Go Scheduler (M:N)  /  Tokio Executor           │
├─────────────────────────────────────────────────┤
│               作業系統層(Kernel)                 │
│  Process / Kernel Thread  context switch          │
├─────────────────────────────────────────────────┤
│               硬體層(CPU)                        │
│  暫存器儲存、TLB 刷新、Cache Miss                  │
└─────────────────────────────────────────────────┘

三、各語言 Context Switch 機制比較

3.1 總覽表

特性C(pthread)C++20 CoroutineRust async/awaitGo Goroutine
並發單元Kernel ThreadCoroutine(stackless)Future(stackless)Goroutine(stackful)
StackOS 分配(預設 8MB)無獨立 Stack無獨立 Stack動態 Stack(初始 2KB)
切換類型OS Context SwitchState Machine 轉移State Machine 轉移Go Runtime 切換
切換成本高(需進 Kernel)極低極低低(User Space)
調度者OS Kernel開發者/框架tokio / async-stdGo Runtime
並發模型1:1(Thread:OS Thread)N:1 or M:N(視框架)M:N(tokio)M:N
記憶體安全手動(不安全)手動(不安全)編譯期保證GC + Race Detector
學習曲線

3.2 C 語言:最接近底層

使用 pthread(Kernel Thread)

#include <pthread.h>

void* worker(void* arg) {
    // 每個 thread 由 OS 調度,切換時是完整的 context switch
    return NULL;
}

int main() {
    pthread_t t;
    pthread_create(&t, NULL, worker, NULL);
    pthread_join(t, NULL);
}

特點:

  • 每個 thread 是 OS Kernel Thread,切換需要 syscall,成本高
  • 預設 Stack 大小 8MB,大量 thread 會耗盡記憶體

使用 ucontext(User Space 協程模擬)

#include <ucontext.h>

ucontext_t ctx1, ctx2;
char stack[64 * 1024];  // 手動分配 Stack

void func2() {
    printf("in func2\n");
    swapcontext(&ctx2, &ctx1);  // 切回 ctx1
}

int main() {
    getcontext(&ctx2);
    ctx2.uc_stack.ss_sp = stack;
    ctx2.uc_stack.ss_size = sizeof(stack);
    ctx2.uc_link = &ctx1;
    makecontext(&ctx2, func2, 0);

    swapcontext(&ctx1, &ctx2);  // 切換到 ctx2
    printf("back in main\n");
}

特點:

  • User Space 切換,不需要進入 Kernel
  • 比 pthread 輕量,但需要手動管理 Stack
  • 是許多協程框架(如 libco、libtask)的底層原語

3.3 C++20:Stackless Coroutine

C++20 引入 co_awaitco_yieldco_return,編譯器自動產生 State Machine

#include <coroutine>
#include <iostream>

struct Generator {
    struct promise_type {
        int value;
        Generator get_return_object() {
            return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(int v) {
            value = v;
            return {};
        }
        void return_void() {}
        void unhandled_exception() {}
    };

    std::coroutine_handle<promise_type> handle;

    int next() {
        handle.resume();  // ← 這裡發生 "context switch"(state machine 轉移)
        return handle.promise().value;
    }
};

Generator counter() {
    for (int i = 0; ; ++i)
        co_yield i;  // ← 暫停點,儲存目前狀態
}

int main() {
    auto gen = counter();
    for (int i = 0; i < 5; ++i)
        std::cout << gen.next() << "\n";
}

切換原理:

co_yield / co_await 遇到時:
  → 編譯器將目前局部變數打包進 coroutine frame(heap 上)
  → 記錄下一個 resume 的位置(類似 goto label)
  → 返回給 caller

resume() 呼叫時:
  → 從 coroutine frame 恢復狀態
  → 跳到上次暫停的位置繼續執行

成本對比:

  • 無 Stack 分配(只有一個 heap object)
  • 切換只是 函式呼叫 + struct 欄位存取,極輕量
  • 缺點:無法在任意深度的呼叫堆疊中 co_await(不像 Go 可以在任何地方被調度)

3.4 Rust:async/await + Future

Rust 的 async 也是 stackless coroutine,但加上了編譯期記憶體安全保證。

use tokio::time::{sleep, Duration};

async fn task_a() {
    println!("Task A: start");
    sleep(Duration::from_millis(100)).await;  // ← 暫停點,讓出執行權
    println!("Task A: done");
}

async fn task_b() {
    println!("Task B: start");
    sleep(Duration::from_millis(50)).await;
    println!("Task B: done");
}

#[tokio::main]
async fn main() {
    tokio::join!(task_a(), task_b());
}

Future 的本質:

#![allow(unused)]
fn main() {
// async fn task_a() 會被編譯器轉成類似這樣的 State Machine:
enum TaskAState {
    Start,
    WaitingSleep(SleepFuture),
    Done,
}

impl Future for TaskA {
    type Output = ();
    fn poll(&mut self, cx: &mut Context) -> Poll<()> {
        match self.state {
            Start => {
                println!("Task A: start");
                self.state = WaitingSleep(sleep(...));
                Poll::Pending  // ← 告訴 runtime 我還沒好
            }
            WaitingSleep(ref mut f) => {
                if f.poll(cx).is_ready() {
                    println!("Task A: done");
                    Poll::Ready(())
                } else {
                    Poll::Pending
                }
            }
        }
    }
}
}

tokio 的 M:N 調度:

┌──────────────────────────────────────┐
│  tokio Runtime                        │
│  ┌────────────┐  ┌────────────┐       │
│  │ Worker 1   │  │ Worker 2   │  ...  │  ← OS Threads(數量 = CPU 核心數)
│  │            │  │            │       │
│  │ Future A   │  │ Future C   │       │
│  │ Future B   │  │ Future D   │       │
│  └────────────┘  └────────────┘       │
│         ↑ poll() / wake() 驅動         │
└──────────────────────────────────────┘

Rust vs Go 記憶體安全:

場景GoRust
跨 Goroutine 共享資料執行期 Race Detector編譯期 Send/Sync trait 檢查
資料競爭go run -race 檢測編譯器直接拒絕
記憶體洩漏GC 自動回收所有權系統自動釋放

3.5 Go:Goroutine + G-M-P 調度模型

Goroutine 的特點

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(100 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()

    for i := 0; i < 3; i++ {
        go worker(ctx, i)  // ← 建立 Goroutine,僅 2KB stack
    }

    <-ctx.Done()
    fmt.Println("All workers stopped")
}

G-M-P 調度模型

G = Goroutine(使用者的並發任務)
M = Machine(OS Thread,真正執行 G 的載體)
P = Processor(調度上下文,持有 run queue)

┌────────────────────────────────────────────────┐
│  Go Runtime Scheduler                           │
│                                                 │
│  P1: [G1, G2, G3, ...]  →  M1 (OS Thread)      │
│  P2: [G4, G5, G6, ...]  →  M2 (OS Thread)      │
│  P3: [G7, ...]          →  M3 (OS Thread)       │
│                                                 │
│  Global Queue: [G8, G9, ...]                    │
└────────────────────────────────────────────────┘

當 G 遇到以下情況時,Go Scheduler 切換 Goroutine:
  - channel 阻塞
  - syscall(Go 會把 M 與 P 分離)
  - time.Sleep
  - runtime.Gosched()
  - 函式呼叫(搶佔式調度,Go 1.14+)

Goroutine 切換 vs OS Context Switch 成本比較:

項目OS Thread Context SwitchGoroutine Switch
觸發方式Kernel 中斷Go Runtime 函式呼叫
儲存的狀態所有 CPU 寄存器 + FPU僅 3 個寄存器(SP, PC, DX)
典型耗時~1–10 μs~100–200 ns
Stack 大小固定(MB 級)動態 2KB–1GB
最大並發數千~萬百萬

四、Go context.Context:應用層的任務控制

這與 OS Context Switch 完全不同,是用來 控制 Goroutine 生命週期 的工具。

context 的樹狀結構

context.Background()
    └── WithCancel(ctx)          ← 可手動取消
        └── WithTimeout(ctx, 5s) ← 5 秒後自動取消
            └── WithValue(ctx, "traceID", "abc123")  ← 攜帶元數據

四種 context 函式

函式用途典型場景
context.Background()根 context,永不取消main / 頂層函式
WithCancel(parent)手動呼叫 cancel() 時取消提前結束子任務
WithTimeout(parent, d)超時自動取消HTTP 請求逾時
WithDeadline(parent, t)指定截止時間點取消定時任務
WithValue(parent, k, v)傳遞請求級別的值Trace ID、Auth Token

實際應用:HTTP 請求鏈

func handleRequest(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
    defer cancel()

    // 傳遞 TraceID
    ctx = context.WithValue(ctx, "traceID", generateTraceID())

    result, err := queryDatabase(ctx)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "timeout", 504)
        }
        return
    }
    // ...
}

func queryDatabase(ctx context.Context) (string, error) {
    select {
    case <-ctx.Done():
        return "", ctx.Err()  // 上游取消了,立即返回
    case result := <-doQuery():
        return result, nil
    }
}

五、各語言協程機制深度對比

Stackful vs Stackless

Stackful Coroutine(Go Goroutine / C ucontext):
  ┌──────────────────────┐
  │ Goroutine A          │
  │  Stack Frame 3       │  ← 可以在任意呼叫深度暫停
  │  Stack Frame 2       │
  │  Stack Frame 1       │
  └──────────────────────┘

Stackless Coroutine(Rust Future / C++20 coroutine):
  ┌──────────────────────┐
  │ Future/State Machine │
  │  { state: Waiting,   │  ← 只儲存暫停點的局部變數
  │    local_var: 42 }   │
  └──────────────────────┘
  注意:只能在 async fn 內使用 await,不能在普通函式中暫停

效能基準(參考值)

操作耗時(參考)
OS Thread Context Switch1,000 – 10,000 ns
Go Goroutine Switch100 – 300 ns
Rust Future Poll(無等待)< 10 ns
C ucontext swapcontext50 – 200 ns
C++20 coroutine resume< 10 ns

記憶體佔用(每個並發單元)

並發單元記憶體最大建議數量
OS Thread(Linux)~8 MB stack~數千
Go Goroutine2KB – 8KB(動態)~百萬
Rust Future依狀態 struct 大小~百萬以上
C ucontext手動設定(通常 64KB+)~數萬

六、選擇指南

你需要什麼?
│
├─ 高效能 I/O 密集型服務(Web API、微服務)
│   ├─ 易用性優先 → Go(Goroutine + context)
│   └─ 效能/控制優先 → Rust(tokio + async)
│
├─ 系統程式 / 嵌入式(記憶體受限)
│   ├─ 傳統多執行緒 → C(pthread)
│   └─ 現代協程 → C++20 coroutine 或 Rust async(no_std)
│
├─ CPU 密集型運算
│   ├─ 所有語言的 OS Thread 都適用
│   └─ Go 的 Goroutine 在 CPU-bound 無明顯優勢
│
└─ 需要細粒度控制生命週期
    └─ 一定要用 Go context.Context 或 Rust 的 CancellationToken

七、總結一句話

語言/概念核心問題
OS Context SwitchCPU 現在要幫誰工作?
C pthread我來直接開 OS Thread,切換交給 Kernel
C ucontext我手動切換,比 Kernel 便宜但你要自己管
C++20 coroutine編譯器幫我產生 State Machine,切換超便宜
Rust async/await同上,但加上編譯期記憶體安全
Go GoroutineRuntime 幫我調度,Stack 自動增長,用起來最簡單
Go context.Context這個任務還需要繼續執行嗎?

本文整理自 OS 原理、C POSIX 文件、C++20 標準、Rust async Book、Go 官方文件