C — 线程与并发

线程是进程内的一个执行流,同一进程内的多个线程共享虚拟地址空间和文件描述符表,但各自拥有独立的寄存器和栈。

线程与进程的对比

维度进程线程
资源独立虚拟地址空间、独立文件描述符表共享地址空间和文件表
创建开销大(复制页表、fork 等)小(只分配栈和 task_struct)
上下文切换需要切换页表(CR3)不需要切换页表(更快)
隔离性强(进程间不能直接访问对方内存)弱(线程间可互相读写)
调度内核以线程为单位调度内核以线程为单位调度

Linux 实际上把线程实现为一种轻量级进程(LWP):用 clone() 系统调用创建线程,通过不同标志位决定父进程的哪些资源被共享。

内核线程 vs 用户线程

类型说明调度者代表
内核线程(1:1)每个用户线程对应一个内核可见的调度实体内核Linux(NPTL)
用户线程(N:1)多个用户线程映射到一个内核线程,在用户态自行调度用户态库GNU Pth(已过时)
混合(M:N)多个用户线程映射到多个内核线程协同Go(goroutine)

Linux 默认采用 1:1 模型(通过 clone + futex 等系统调用实现),这也是所有主流编程语言线程的标准实现。

竞争条件(Race Condition)

多个线程同时访问共享数据,且至少有一个是写操作,最终结果取决于线程的执行顺序:

// 两个线程同时执行 counter++
// counter++ 不是原子操作!它被编译为三条汇编指令:
// mov eax, [counter] ; 1. 从内存读
// add eax, 1 ; 2. 加一
// mov [counter], eax ; 3. 写回内存
// 如果两线程交错执行这 3 条指令,最终 counter 可能只加了 1 而非 2

对容器的意义:多个线程同时操作 std::vector(一端 push_back 一端读取容量)或 std::map(同时插入两个节点)会破坏内部数据结构。即使单次操作的线程安全可能有保证(如 const 成员函数是读操作),但容器整体不是线程安全的

原子操作

原子操作是不可分割的 CPU 指令,不会被其他线程打断。x86 提供 LOCK 前缀实现原子性:

#include <stdatomic.h> // C11 原子类型
atomic_int counter = 0;
atomic_fetch_add(&counter, 1); // 原子递增,编译为 lock xadd
操作指令(x86)说明
原子加lock xadd读-修改-写,保证原子性
CASlock cmpxchgCompare-And-Swap,无锁数据结构的基础
内存屏障mfence / lfence / sfence保证访存顺序

CAS 是无锁数据结构(lock-free queue、lock-free stack、无锁哈希表)的核心原语。这类数据结构在多线程下不需要互斥锁,避免了锁的开销和死锁风险。

缓存一致性与多核

多核 CPU 各自有 L1/L2 缓存。当一个核修改了某条 cache line,另一个核读到旧数据就是数据不一致。CPU 通过缓存一致性协议(MESI)自动维护一致性:

状态含义
Modified(修改)该 cache line 在当前核被修改,其他核无效
Exclusive(独占)仅当前核持有,数据与主存一致
Shared(共享)多个核持有,均为只读
Invalid(无效)cache line 不可用

两个核对同一条 cache line 做写入 → Modified → Shared 的 bounce → 频繁传递,性能下降。

对容器的意义:两个线程分别写 vector 中相邻的两个 int,它们在同一条 cache line 中。即使每个线程只写”自己的那一部分”,缓存一致性协议也会导致 cache line 在核间来回传递(伪共享)。

本章与其他模块的链接