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