跳轉到

2011:C11——執行緒、原子操作與泛型#

事件#

2011 年,ISO 發佈 ISO/IEC 9899:2011(C11)。多核心 CPU 已全面普及,但 C90/C99 的語言層級完全沒有執行緒與記憶體模型概念,並發程式全靠平台 API(pthreads、Windows threads)自求多福。C11 為 C 語言補上並發理論的基礎。

為何重要#

  • 新語法一覽:_Thread_local、threads.h(標準執行緒庫)、_Atomic 與 stdatomic.h、C 記憶體模型(與 C++11 共享)、_Generic 泛型選擇、_Static_assert 編譯期斷言、匿名 struct/union、_Alignas/_Alignof、char16_t/char32_t 與 Unicode 支援、邊界檢查介面 Annex K。
  • 與 C++11 同步:記憶體模型與原子操作直接和 C++11 委員會合作制定,確保兩語言併行不悖。

理論與實用原因(各新語法為何加入)#

  • C 記憶體模型與 _Atomic:
  • 彌補的缺陷:C99 沒有定義「兩個執行緒同時讀寫變數」的行為——data race 是未定義行為,編譯器最佳化(快取到暫存器、指令重排)讓並發程式在多核心上隨機爆炸。
  • 理論原因:採用學界成熟的「記憶體模型」理論(循序一致性 + acquire/release 語意),讓「並發程式的行為」第一次有了數學定義——這是 C 語言語意理論上最大的一次補課。
  • 實用原因:atomic_int、atomic_fetch_add 讓無鎖(lock-free)程式設計有標準語法。
  • threads.h 標準執行緒:
  • 彌補的缺陷:C 程式用執行緒必須依賴 pthreads(POSIX)或 Win32 API,程式碼不可攜。
  • 實用原因:與 C++11 的 <thread> 對應,讓執行緒成為語言標準的一部分。
  • _Generic 泛型選擇:
  • 彌補的缺陷:C 沒有函式多載,fabs/fabsf/fabsl 各自為名,呼叫端要自己挑型別版本。
  • 理論原因:不求完整的泛型系統(C++ templates 太重),只做「依型別選擇表示式」的最小機制——保持 C「少而精」的哲學。
  • 實用原因:<tgmath.h> 的數學函式可用同一個名字服務所有浮點型別。
  • _Static_assert:
  • 彌補的缺陷:struct 大小、平台假設等錯誤要到執行期才發現。
  • 實用原因:_Static_assert(sizeof(int)==4, "...") 讓平台假設在編譯期被驗證。
  • 匿名 struct/union:
  • 彌補的缺陷:巢狀 struct 存取要寫完整路徑(s.inner.x),OS 核心中過度冗長。
  • 實用原因:匿名巢狀讓 s.x 直接存取,語法更簡潔。
  • VLA 降為選用:
  • 彌補的缺陷:C99 強制 VLA 有堆疊溢位風險(陣列長度是使用者輸入時可能爆栈),C11 將其改為選用特性,反映「標準化過頭再回收」的修正。

彌補了什麼缺陷#

C11 彌補了 C 語言「無記憶體模型、並發行為未定義、執行緒不可攜、無泛型、無編譯期斷言」的理論與實用缺陷——並發理論的補課尤其關鍵,讓 C 在多核心時代繼續可用。

程式範例#

C99 的缺陷——data race 是未定義行為,編譯器最佳化讓並發程式隨機爆炸:

/* 沒有記憶體模型的 C:兩執行緒讀寫同一變數,行為未定義 */
int flag = 0;            /* 兩個執行緒同時存取 */

/* 執行緒 A */           while (!flag) ;   /* 等待 */
/* 執行緒 B */           flag = 1;         /* 設定 */
/* 問題:編譯器把 !flag 快取到暫存器,只讀一次——A 永遠等不到,死迴圈! */
/* 或者:指令重排讓 flag = 1 在資料寫入「之前」被看到 */

C11 的 _Atomic 與記憶體模型——並發行為第一次有數學定義:

#include <stdatomic.h>

/* _Atomic:data race 不再未定義,且有標準的無鎖操作 */
atomic_int flag = 0;

atomic_store(&flag, 1);                    /* 執行緒 B:設定(含記憶體屏障) */
while (atomic_load(&flag) == 0) ;          /* 執行緒 A:每次都真的讀 */

atomic_int counter = 0;
atomic_fetch_add(&counter, 1);             /* 原子加一——無鎖計數器 */

threads.h——執行緒終於可攜,不再依賴 pthreads/Win32:

#include <threads.h>
#include <stdio.h>

int worker(void *arg) {          /* 執行緒函式:回傳 int 而非 void * */
    printf("thread %d running\n", *(int *)arg);
    return 0;
}

int main(void) {
    thrd_t t;
    int id = 1;
    thrd_create(&t, worker, &id);   /* 標準執行緒——Windows/Linux 都能編譯 */
    thrd_join(t, 0);                /* 等待結束 */
    return 0;
}

_Generic——依型別自動選擇,補上「無函式多載」的缺陷:

#include <stdio.h>

/* C99 的缺陷:fabs/fabsf/fabsl 各自為名,呼叫端要自己挑 */
double  d = fabs(1.5);
float   f = fabsf(1.5f);        /* 要記三個名字! */
long double l = fabsl(1.5L);

/* C11:一個名字服務所有型別 */
#define ABS(x) _Generic((x),       \
    float:       fabsf,            \
    double:      fabs,             \
    long double: fabsl,            \
    default:     fabs              \
)(x)

f = ABS(1.5f);        /* 編譯器依 x 的型別自動選 fabsf */
d = ABS(1.5);         /* 自動選 fabs */

_Static_assert——平台假設在編譯期被驗證:

#include <assert.h>

_Static_assert(sizeof(int) == 4, "此程式需要 32 位元 int");
_Static_assert(sizeof(void *) >= sizeof(long), "指標必須能放進 long");

struct packet {
    unsigned char data[6];
};
_Static_assert(sizeof(struct packet) == 6, "封包大小必須精確 6 bytes");
/* 若平台對齊改變了 struct 大小,編譯期就報錯,不用等執行期 */

匿名 struct/union——巢狀存取不再冗長:

/* C99:巢狀存取要寫完整路徑 */
struct event { int type; struct { int x; int y; } pos; };
struct event e;
e.pos.x = 1;             /* 冗長 */

/* C11:匿名巢狀 */
struct event { int type; struct { int x; int y; }; };
struct event e;
e.x = 1;                 /* 直接存取 */

VLA 降為選用——堆疊溢位風險的回收:

/* C99:VLA 強制支援,使用者輸入長度時可能爆栈 */
void risky(int n) { int buf[n]; ... }    /* n = 100000000?堆疊溢位! */

/* C11:VLA 變成選用特性(__STDC_NO_VLA__ 可偵測),鼓勵用 malloc 或固定上限 */
#ifdef __STDC_NO_VLA__
    /* 此編譯器不支援 VLA——用 malloc 取代 */
#endif

相關條目#