Linux 多线程编程基础入门

Linux 多线程编程基础入门

集群 : ecs-57c4-0004 (华为云香港 FlexusX, 8vCPU/16GiB, Ubuntu 24.04, gcc 13.3.0)

文档版本 : v1.0 | 创建日期 : 2026-06-17

涵盖实验: 线程基本概念 · 线程高级话题 · 线程互斥与同步


目录

  1. 线程的基本概念
  • 1.1 线程 vs 进程
  • 1.2 pthread_create / pthread_join
  • 1.3 线程终止:return vs pthread_exit
  • 1.4 pthread_detach:分离线程
  • 1.5 线程栈空间
  1. 线程的高级话题
  • 2.1 pthread_attr_t:线程属性
  • 2.2 pthread_once:一次性初始化
  • 2.3 线程特有数据 (TSD)
  • 2.4 pthread_cancel:线程取消
  1. 线程的互斥与同步
  • 3.1 互斥锁 (mutex)
  • 3.2 互斥锁类型:ERRORCHECK / RECURSIVE
  • 3.3 条件变量 (cond)
  • 3.4 读写锁 (rwlock)
  • 3.5 POSIX 信号量 (semaphore)
  • 3.6 屏障 (barrier)
  • 3.7 同步原语性能对比

环境准备

bash 复制代码
# 编译环境 (gcc 13.3.0, GNU libc)
sudo apt-get install -y build-essential

# 线程库: libpthread 已内置在 glibc 中, 编译时指定 -lpthread

# 验证
$ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0

$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.39

$ getconf _NPROCESSORS_ONLN
8

1. 线程的基本概念

1.1 线程 vs 进程

维度 进程 (Process) 线程 (Thread)
资源 独立地址空间、文件描述符 共享进程地址空间
创建开销 大 (fork + exec) 小 (共享页表)
通信 IPC (管道/共享内存/消息队列) 直接读写共享变量
调度 内核调度 内核调度 (NPTL 1:1 模型)
隔离性 强 (一个崩溃不影响其他) 弱 (一个线程崩溃 → 进程崩溃)
栈空间 独立 (通常 8MB) 独立 (默认 8MB, 可调整)
复制代码
  ┌───────────────────────────────────────────┐
  │            Process (PID=10868)            │
  │                                           │
  │  ┌──────────┐ ┌──────────┐ ┌──────────┐  │
  │  │ Thread 1 │ │ Thread 2 │ │ Thread 3 │  │
  │  │ TID=10869│ │ TID=10870│ │ TID=10871│  │
  │  │ 栈: 8MB  │ │ 栈: 8MB  │ │ 栈: 8MB  │  │
  │  └──────────┘ └──────────┘ └──────────┘  │
  │                                           │
  │  共享: .text .data .bss heap fd           │
  └───────────────────────────────────────────┘

1.2 pthread_create / pthread_join

核心 API:

c 复制代码
#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);
int pthread_join(pthread_t thread, void **retval);

完整代码 (exp1_thread_basics.c):

c 复制代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/syscall.h>

void *thread_func(void *arg) {
    long id = (long)arg;
    printf("  [子线程 %ld] PID=%d  TID(kernel)=%ld  pthread_self()=%p\n",
           id, getpid(), syscall(SYS_gettid), (void*)pthread_self());
    printf("  [子线程 %ld] 收到参数 arg=%ld\n", id, id);
    printf("  [子线程 %ld] 开始处理...\n", id);
    sleep(1);
    printf("  [子线程 %ld] 处理完成, 准备退出\n", id);
    return (void*)(id * 100);  // 返回值
}

int main() {
    printf("  [主线程] PID=%d  TID(kernel)=%ld  pthread_self()=%p\n",
           getpid(), syscall(SYS_gettid), (void*)pthread_self());

    pthread_t threads[3];
    for (long i = 1; i <= 3; i++) {
        pthread_create(&threads[i-1], NULL, thread_func, (void*)i);
        printf("  主线程: 子线程 %ld 已创建 (pthread_t=%p)\n", i, (void*)threads[i-1]);
    }

    printf("\n--- 等待所有子线程结束 (pthread_join) ---\n");
    for (int i = 0; i < 3; i++) {
        void *ret;
        pthread_join(threads[i], &ret);
        printf("  主线程: 子线程 %d 已结束, 返回值=%ld\n", i+1, (long)ret);
    }
    return 0;
}

运行输出:

复制代码
  [主线程] PID=10868  TID(kernel)=10868  pthread_self()=0x722dc3012740

--- 创建 3 个子线程 ---
  主线程: 子线程 1 已创建 (pthread_t=0x722dc2dff6c0)

[子线程 1 启动]
  [子线程] PID=10868  TID(kernel)=10869  pthread_self()=0x722dc2dff6c0
  [子线程 1] 收到参数 arg=1
  [子线程 1] 开始处理...
  主线程: 子线程 2 已创建 (pthread_t=0x722dc25fe6c0)

[子线程 2 启动]
  [子线程] PID=10868  TID(kernel)=10870  pthread_self()=0x722dc25fe6c0
  [子线程 2] 收到参数 arg=2
  [子线程 2] 开始处理...
  主线程: 子线程 3 已创建 (pthread_t=0x722dc1dfd6c0)

[子线程 3 启动]
  [子线程] PID=10868  TID(kernel)=10871  pthread_self()=0x722dc1dfd6c0
  [子线程 3] 收到参数 arg=3
  [子线程 3] 开始处理...

--- 等待所有子线程结束 (pthread_join) ---
  主线程: 子线程 1 已结束, 返回值=100
  主线程: 子线程 2 已结束, 返回值=200
  主线程: 子线程 3 已结束, 返回值=300

  ✓ 所有子线程已 join, 主线程继续执行

关键观察:

  • 所有线程共享同一个 PID (10868),区分靠内核 TID (gettid)
  • pthread_self() 返回的是库级标识,不同于内核 TID
  • pthread_join 阻塞等待,获取返回值后清理线程资源
  • 线程执行顺序不确定 --- 创建顺序 ≠ 执行顺序

1.3 线程终止:return vs pthread_exit

c 复制代码
void *thread_return(void *arg) {
    printf("  [return 方式] 通过 return 退出 (推荐)\n");
    return (void*)42;
}

void *thread_pthread_exit(void *arg) {
    printf("  [pthread_exit 方式] 通过 pthread_exit() 退出\n");
    pthread_exit((void*)43);
}

对比:

方式 行为 适用场景
return val 等价于 pthread_exit(val) 线程函数顶层返回,推荐
pthread_exit(val) 在任意调用栈深度退出当前线程 深层嵌套中终止线程
exit(n) 终止整个进程(包括所有线程!) 禁止在线程中使用
复制代码
  return val      → 堆栈正常展开,析构局部变量
  pthread_exit()  → 可在任意深度退出当前线程
  exit()          → 终止整个进程 ← 小心!

1.4 pthread_detach:分离线程

分离线程的特点:线程结束后自动回收资源,不可 join,不可获取返回值

c 复制代码
pthread_t t1, t2;
pthread_create(&t1, NULL, detach_worker, NULL);
pthread_create(&t2, NULL, detach_worker, NULL);

pthread_detach(t1);
pthread_detach(t2);

// pthread_join(t1, NULL);  ← 失败: Invalid argument

运行输出:

复制代码
  线程 1 已分离 (pthread_t=0x722dc1dfd6c0)
  [分离线程 1] 正在工作...
  线程 2 已分离 (pthread_t=0x722dc25fe6c0)
  [分离线程 2] 正在工作...

  ⚠ pthread_join(t1) 失败: Invalid argument
  → 分离线程不可 join, 资源在线程结束时自动回收
  主线程休眠 2 秒等待分离线程完成...
  [分离线程 1] 工作完成, 自动释放资源
  [分离线程 2] 工作完成, 自动释放资源

Joinable vs Detached:

复制代码
  Joinable (默认)          Detached (分离)
  ┌──────────┐            ┌──────────┐
  │ pthread  │            │ pthread  │
  │ create   │            │ create   │
  └────┬─────┘            └────┬─────┘
       │                       │
       ▼                       ▼
  ┌──────────┐            ┌──────────┐
  │ 线程运行  │            │ 线程运行  │
  └────┬─────┘            └────┬─────┘
       │                       │
       ▼                       ▼
  ┌──────────┐            ┌──────────┐
  │ 保留退出  │            │ 自动释放  │
  │ 状态等待  │            │ 全部资源  │
  │ join 回收 │            │ (不可join)│
  └──────────┘            └──────────┘

使用场景:

  • 守护线程 (不需要获取返回值)
  • 网络连接处理线程 (fire-and-forget)
  • 注意: 分离线程若访问局部变量,需确保变量在线程结束前不被释放

1.5 线程栈空间

复制代码
  [栈信息] 地址=0x722dc1dff000  大小=8388608 bytes (8192.0 KB)

每个线程默认栈空间 = 8MB。计算最大线程数:

复制代码
8vCPU × 16GiB RAM → 若全部给线程栈, 理论最大 = 16GB / 8MB ≈ 2000 个
实际: 受限于虚拟地址空间 (/proc/sys/kernel/threads-max)
本机: /proc/sys/kernel/threads-max = 125671

2. 线程的高级话题

2.1 pthread_attr_t:线程属性

c 复制代码
pthread_attr_t attr;
pthread_attr_init(&attr);

// 获取默认属性
size_t stacksize;
int detachstate;
pthread_attr_getstacksize(&attr, &stacksize);
pthread_attr_getdetachstate(&attr, &detachstate);

printf("  栈大小:     %lu bytes (%.1f KB)\n", stacksize, stacksize/1024.0);
printf("  分离状态:   %s\n", detachstate==PTHREAD_CREATE_JOINABLE ? "JOINABLE" : "DETACHED");

// 自定义属性
pthread_attr_setstacksize(&attr, 1024 * 1024);   // 1MB 栈
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

pthread_create(&t, &attr, custom_attr_worker, NULL);
pthread_attr_destroy(&attr);

运行输出:

复制代码
--- 默认属性 ---
  [自定义属性线程]
    栈大小:     8388608 bytes (8192.0 KB)
    分离状态:   JOINABLE

--- 自定义属性 (DETACHED + 1MB栈) ---
  设置栈大小: 1048576 bytes (1024 KB)
  ⚠ pthread_join 失败 (预期): Invalid argument
  [自定义属性线程]
    栈大小:     1048576 bytes (1024.0 KB)
    分离状态:   DETACHED

可调整属性:

属性 获取/设置 说明
stacksize pthread_attr_get/setstacksize() 默认 8MB
detachstate pthread_attr_get/setdetachstate() JOINABLE / DETACHED
schedpolicy pthread_attr_getschedpolicy() SCHED_FIFO / SCHED_RR / SCHED_OTHER
inheritsched pthread_attr_get/setinheritsched() 是否继承调度属性
guardsize pthread_attr_get/setguardsize() 栈保护页大小 (默认 4KB)

2.2 pthread_once:一次性初始化

场景: 多个线程需要初始化同一份全局资源(如日志系统、连接池),但初始化必须且仅执行一次。

c 复制代码
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int global_resource;

void init_global_resource(void) {
    printf("  [ONCE] 全局资源初始化 (执行者: TID=%ld)\n", syscall(SYS_gettid));
    global_resource = 42;
    printf("  [ONCE] 初始化完成: global_resource = %d\n", global_resource);
}

void *once_worker(void *arg) {
    long id = (long)arg;
    printf("  [线程 %ld, TID=%ld] 调用 pthread_once...\n", id, syscall(SYS_gettid));
    pthread_once(&once_control, init_global_resource);
    printf("  [线程 %ld, TID=%ld] 继续: global_resource = %d\n",
           id, syscall(SYS_gettid), global_resource);
    return NULL;
}

运行输出 (5 个线程竞争):

复制代码
  [线程 1, TID=10880] 调用 pthread_once...
  [ONCE] 全局资源初始化 (执行者: TID=10880)
  [线程 2, TID=10881] 调用 pthread_once...
  [线程 3, TID=10882] 调用 pthread_once...
  [线程 4, TID=10883] 调用 pthread_once...
  [线程 5, TID=10884] 调用 pthread_once...
  [ONCE] 初始化完成: global_resource = 42
  [线程 1, TID=10880] 继续: global_resource = 42
  [线程 2, TID=10881] 继续: global_resource = 42
  [线程 3, TID=10882] 继续: global_resource = 42
  [线程 4, TID=10883] 继续: global_resource = 42
  [线程 5, TID=10884] 继续: global_resource = 42

  ✓ 5 个线程竞争, 但 init_global_resource() 仅执行了 1 次

原理:

复制代码
  线程1 ──→ pthread_once ──→ 执行 init (TID=10880)
  线程2 ──→ pthread_once ──→ 阻塞等待
  线程3 ──→ pthread_once ──→ 阻塞等待
  线程4 ──→ pthread_once ──→ 阻塞等待
  线程5 ──→ pthread_once ──→ 阻塞等待
                                      │
         所有线程 ── init 完成 ──→ 继续执行

典型应用:

  • 日志系统初始化
  • 数据库连接池初始化
  • 配置文件加载 (解析一次, 所有线程共享)

2.3 线程特有数据 (TSD)

线程特有数据为每个线程维护一份独立的数据副本,由析构函数在线程退出时自动回收。

c 复制代码
pthread_key_t tsd_key;

void tsd_destructor(void *data) {
    printf("  [TSD 析构] TID=%ld 释放 data=%ld\n",
           syscall(SYS_gettid), (long)data);
    free(data);
}

void *tsd_worker(void *arg) {
    long *data = malloc(sizeof(long));
    *data = (long)arg;
    pthread_setspecific(tsd_key, data);

    printf("  [线程 %ld, TID=%ld] 设置 TSD = %ld\n",
           (long)arg, syscall(SYS_gettid), *data);
    sleep(1);

    long *read = pthread_getspecific(tsd_key);
    printf("  [线程 %ld, TID=%ld] 读取 TSD = %ld\n",
           (long)arg, syscall(SYS_gettid), *read);
    return NULL;
}

// 初始化
pthread_key_create(&tsd_key, tsd_destructor);

运行输出:

复制代码
  [线程 1, TID=10885] 设置 TSD = 100
  [线程 3, TID=10887] 设置 TSD = 300
  [线程 2, TID=10886] 设置 TSD = 200
  [线程 1, TID=10885] 读取 TSD = 100
  [TSD 析构] TID=10885 释放 data=100
  [线程 3, TID=10887] 读取 TSD = 300
  [线程 2, TID=10886] 读取 TSD = 200
  [TSD 析构] TID=10887 释放 data=300
  [TSD 析构] TID=10886 释放 data=200

对比 errno (TSD 的经典应用):

复制代码
  线程1: errno = EAGAIN     ← 线程独立副本
  线程2: errno = EINTR      ← 互不影响
  线程3: errno = 0

TSD vs 传参:

方案 优点 缺点
传参 (void*)arg 简单 调用栈深层函数无法访问
全局变量 所有函数可访问 需要加锁保护
TSD 线程安全 + 全局可访问 + 自动析构 额外内存开销

2.4 pthread_cancel:线程取消

取消状态与类型:

c 复制代码
// 控制项: 是否可取消 (ENABLE/DISABLE)
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);  // 禁止取消
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);   // 允许取消

// 取消类型: 立即取消 vs 延迟取消
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);  // 立即
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);      // 延迟 (默认)

案例1: 取消可取消线程 (默认)

复制代码
  [可取消线程, TID=10944] 启动 (延迟取消模式)
  [可取消线程, TID=10944] 迭代 1
  [可取消线程, TID=10944] 迭代 2
  [可取消线程, TID=10944] 迭代 3
  主线程: 发送取消请求...
  [可取消线程, TID=10944] 迭代 4
  ✓ 线程已取消 (返回值 = PTHREAD_CANCELED = 0xffffffffffffffff)

PTHREAD_CANCELED = (void*)-1 → 在线程函数中表示为 0xffffffffffffffff

案例2: 取消已禁用的线程

复制代码
  [不可取消线程, TID=10945] 启动 (取消已禁用)
  [不可取消线程, TID=10945] 迭代 1 (不可取消)
  [不可取消线程, TID=10945] 迭代 2 (不可取消)
  主线程: 发送取消请求...
  [不可取消线程, TID=10945] 迭代 3 (不可取消)
  [不可取消线程, TID=10945] 迭代 4 (不可取消)
  [不可取消线程, TID=10945] 迭代 5 (不可取消)
  ✓ 线程正常结束 (取消请求被忽略, 因 PTHREAD_CANCEL_DISABLE)

案例3: 带清理函数

c 复制代码
void cleanup_handler(void *arg) {
    printf("  [清理函数] 线程被取消, 释放资源: %s\n", (char*)arg);
    // 实际场景: free(buf), close(fd), unlock(mutex) ...
}

void *cleanup_worker(void *arg) {
    pthread_cleanup_push(cleanup_handler, "已分配内存 / 文件句柄");
    while (1) {
        printf("  [带清理线程, TID=%ld] 步骤 %d\n", syscall(SYS_gettid), step++);
        sleep(1);
        pthread_testcancel();  // 取消点
    }
    pthread_cleanup_pop(1);  // 1 = 执行清理函数
    return NULL;
}

输出:

复制代码
  [带清理线程, TID=10946] 步骤 1
  [带清理线程, TID=10946] 步骤 2
  主线程: 发送取消请求...
  [清理函数] 线程被取消, 释放资源: 已分配内存 / 文件句柄
  ✓ 清理函数已执行, 资源已释放

取消点:

必定取消点 可能取消点
pthread_cond_wait() open(), read(), write()
pthread_testcancel() printf(), fprintf()
sleep(), nanosleep() accept(), connect()
sem_wait() pthread_mutex_lock()

3. 线程的互斥与同步

3.1 互斥锁 (mutex)

数据竞争示例 (无锁):

c 复制代码
int shared_counter = 0;  // ← 无保护

void *increment(void *arg) {
    for (int i = 0; i < 100000; i++)
        shared_counter++;  // 不是原子操作!
    return NULL;
}

加锁版本:

c 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;

void *increment_safe(void *arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        shared_counter++;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

运行输出 (8vCPU 环境下,4 个线程各 10 万次):

复制代码
--- 无锁: 4 线程各递增 100,000 次 ---
  期望值: 400000
  实际值: 400000  ← 一致 (巧合)

--- 加锁: 4 线程各递增 100,000 次 ---
  期望值: 400000
  实际值: 400000  ← ✓ 正确

注意 : 无锁版本次输出为 400000 纯属巧合。在更高并发或不同 CPU 架构下,数据竞争通常导致结果小于期望值。永远不要依赖巧合

count++ 为什么不是原子的?

复制代码
  count++  对应 3 条汇编指令:
  mov  eax, [count]   ← 从内存加载
  inc  eax            ← 自增
  mov  [count], eax   ← 写回内存

  线程A: mov eax, [count]  → eax=100
  线程B: mov eax, [count]  → eax=100  (也读了 100!)
  线程A: inc eax            → eax=101
  线程B: inc eax            → eax=101
  线程A: mov [count], eax   → count=101
  线程B: mov [count], eax   → count=101  ← 丢了一次更新!

3.2 互斥锁类型

三种类型对比:

类型 重复 lock 适用场景
PTHREAD_MUTEX_NORMAL 死锁 简单临界区
PTHREAD_MUTEX_ERRORCHECK 返回 EDEADLK (35) 调试/检测死锁
PTHREAD_MUTEX_RECURSIVE 计数+1 (需等量 unlock) 递归函数

实操输出:

复制代码
--- PTHREAD_MUTEX_ERRORCHECK ---
  第一次 lock: 成功
  第二次 lock: 返回 35 (Resource deadlock avoided)

--- PTHREAD_MUTEX_RECURSIVE ---
  第1次 lock: 成功
  第2次 lock: 成功 (递归锁允许多次加锁)
  第3次 lock: 成功
  3 次 unlock: 锁已完全释放

初始化方式:

c 复制代码
// 静态初始化 (默认 NORMAL)
pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER;

// 动态初始化 (指定类型)
pthread_mutex_t m2;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_init(&m2, &attr);
pthread_mutexattr_destroy(&attr);

3.3 条件变量 (pthread_cond_t)

条件变量解决"等待某个条件成立"的经典问题,避免忙等待 (busy-wait)。

c 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int buffer = -1;

// 消费者
void *consumer(void *arg) {
    pthread_mutex_lock(&mutex);
    while (buffer < 0)              // ← 必须用 while, 非 if
        pthread_cond_wait(&cond, &mutex);  // 原子: unlock+wait+lock
    printf("  [消费者] 收到数据: buffer=%d\n", buffer);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

// 生产者
void *producer(void *arg) {
    sleep(1);  // 模拟生产耗时
    pthread_mutex_lock(&mutex);
    buffer = 42;
    printf("  [生产者] 数据就绪: buffer=%d\n", buffer);
    pthread_cond_signal(&cond);   // 唤醒一个等待线程
    pthread_mutex_unlock(&mutex);
    return NULL;
}

signal vs broadcast:

复制代码
--- 生产者-消费者 (signal) ---
  [消费者, TID=11218] 等待数据...
  [生产者, TID=11219] 开始生产数据...
  [生产者, TID=11219] 数据就绪: buffer=42
  [生产者, TID=11219] 发送信号唤醒消费者
  [消费者, TID=11218] 收到数据: buffer=42

--- broadcast: 唤醒所有消费者 ---
  [消费者 1, TID=11220] 等待...
  [消费者 2, TID=11221] 等待...
  [消费者 3, TID=11222] 等待...
  [生产者] 广播唤醒所有消费者
  [消费者 1, TID=11220] 被唤醒! buffer=999
  [消费者 3, TID=11222] 被唤醒! buffer=999
  [消费者 2, TID=11221] 被唤醒! buffer=999

伪唤醒 (spurious wakeup):

c 复制代码
// ❌ 错误: while → if
if (buffer < 0)
    pthread_cond_wait(&cond, &mutex);

// ✓ 正确: 必须用 while
while (buffer < 0)
    pthread_cond_wait(&cond, &mutex);

内核可能因信号中断等原因在没有 signal 的情况下唤醒线程 → 必须用 while 重新检查条件。

3.4 读写锁 (pthread_rwlock_t)

读写锁允许多个读者同时持有读锁,但写者独占。

c 复制代码
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int shared_data = 0;

void *reader(void *arg) {
    pthread_rwlock_rdlock(&rwlock);
    printf("  [读者 %d] 读取: shared_data=%d\n", id, shared_data);
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

void *writer(void *arg) {
    pthread_rwlock_wrlock(&rwlock);
    shared_data = value;
    printf("  [写者] 写入: shared_data=%d\n", shared_data);
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

运行输出 (3 读者 + 1 写者):

复制代码
  [写者] 写入: shared_data=100
  [读者 1] 读取: shared_data=100
  [读者 3] 读取: shared_data=100
  [读者 2] 读取: shared_data=100
  [读者 1] 读取: shared_data=100
  [读者 3] 读取: shared_data=100
  [读者 2] 读取: shared_data=100
  [写者] 写入: shared_data=200
  [读者 1] 读取: shared_data=200
  [读者 3] 读取: shared_data=200
  [读者 2] 读取: shared_data=200
  [写者] 写入: shared_data=300

读写锁规则:

复制代码
  多读共享:     R1──┐            R3──┐
                ...│ 并发读取    ...│
                Rn──┘            Rn──┘

  读写互斥:     W ─── 排他 ─── W
                (其他读者/写者阻塞)

  写写互斥:     W ─── 排他 ─── W

  适合: 读多写少场景 (缓存、配置)

3.5 POSIX 信号量 (sem_t)

计数信号量控制同时访问资源的线程数量。

c 复制代码
#include <semaphore.h>

sem_t sem;
sem_init(&sem, 0, 3);  // 0=进程内, 3=最大并发数

void *worker(void *arg) {
    sem_wait(&sem);   // --count, count=0 时阻塞
    printf("  [线程 %d] ✓ 获得资源, 工作中...\n", id);
    sleep(1);
    sem_post(&sem);   // ++count, 唤醒等待线程
    return NULL;
}

sem_destroy(&sem);

运行输出 (5 线程争 3 资源):

复制代码
  [线程 1, TID=11227] 等待信号量 (获取资源)...
  [线程 1, TID=11227] ✓ 获得资源, 工作中...
  [线程 1]   当前并发数: 1
  [线程 2, TID=11228] 等待信号量 (获取资源)...
  [线程 2, TID=11228] ✓ 获得资源, 工作中...
  [线程 2]   当前并发数: 2
  [线程 3, TID=11229] 等待信号量 (获取资源)...
  [线程 3, TID=11229] ✓ 获得资源, 工作中...
  [线程 3]   当前并发数: 3
  [线程 4, TID=11230] 等待信号量 (获取资源)...  ← 阻塞!
  [线程 5, TID=11231] 等待信号量 (获取资源)...  ← 阻塞!
  [线程 1, TID=11227] 释放资源
  [线程 2, TID=11228] 释放资源
  [线程 4, TID=11230] ✓ 获得资源, 工作中...      ← 释放后获得
  [线程 4]   当前并发数: 2
  [线程 5, TID=11231] ✓ 获得资源, 工作中...

信号量 vs 互斥锁:

复制代码
  mutex (二元信号量):
  ┌──────┐
  │  1   │  仅 1 个线程进入
  └──────┘
     ↑
  并发数=0 时阻塞

  semaphore (计数信号量):
  ┌──────┐
  │  3   │  最多 3 个线程并发
  └──────┘
     ↑
  并发数=0 时阻塞

  典型应用: 连接池、令牌桶、限流器

3.6 屏障 (pthread_barrier_t)

屏障确保所有线程到达同一点后再继续,适合分阶段并行计算。

c 复制代码
pthread_barrier_t barrier;
pthread_barrier_init(&barrier, NULL, 4);  // 4=参与线程数

void *phase_worker(void *arg) {
    printf("  [线程 %d] 阶段1: 准备中...\n", id);
    sleep(id * 0.5);
    printf("  [线程 %d] 到达屏障, 等待其他线程...\n", id);

    int rc = pthread_barrier_wait(&barrier);
    if (rc == PTHREAD_BARRIER_SERIAL_THREAD)
        printf("  [线程 %d] ← 被选为串行线程, 执行收尾工作\n", id);

    printf("  [线程 %d] 阶段2: 所有线程同步启动!\n", id);
    pthread_barrier_wait(&barrier);
    return NULL;
}

运行输出 (4 线程, 2 阶段):

复制代码
  [线程 1] 阶段1: 准备中...
  [线程 2] 阶段1: 准备中...
  [线程 3] 阶段1: 准备中...
  [线程 4] 阶段1: 准备中...
  [线程 3] 到达屏障, 等待其他线程...
  [线程 4] 到达屏障, 等待其他线程...
  [线程 1] 到达屏障, 等待其他线程...
  [线程 2] 到达屏障, 等待其他线程...
  [线程 2] ← 被选为串行线程, 执行收尾工作

  [线程 2] 阶段2: 所有线程同步启动!
  [线程 3] 阶段2: 所有线程同步启动!
  [线程 4] 阶段2: 所有线程同步启动!
  [线程 1] 阶段2: 所有线程同步启动!

屏障的典型应用:

复制代码
  MapReduce:
  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐
  │ Worker 1    │  │ Worker 2    │  │ Worker 3    │
  │ mapPhase()  │  │ mapPhase()  │  │ mapPhase()  │
  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘
         │                │                │
         └────────────────┼────────────────┘
                          │
              ██████████████████████
              █  BARRIER (同步)  █
              ██████████████████████
                          │
         ┌────────────────┼────────────────┐
         │                │                │
  ┌──────┴──────┐  ┌──────┴──────┐  ┌──────┴──────┐
  │ Worker 1    │  │ Worker 2    │  │ Worker 3    │
  │ reducePhase │  │ reducePhase │  │ reducePhase │
  └─────────────┘  └─────────────┘  └─────────────┘

3.7 同步原语性能对比

在 8vCPU FlexusX 上,单线程 lock/unlock 一百万次:

复制代码
--- mutex lock/unlock × 1000000 次 (单线程) ---
  mutex:    0.0058 秒  (5.8 ns/op)

--- spinlock lock/unlock × 1000000 次 (单线程) ---
  spinlock: 0.0047 秒  (4.7 ns/op)

mutex vs spinlock:

维度 mutex spinlock
无竞争耗时 ~5.8 ns ~4.7 ns
竞争时行为 线程休眠 (上下文切换) 忙等待 (CPU 空转)
适用临界区 较长 (微秒级) 极短 (纳秒级)
CPU 友好性 释放 CPU 给其他线程 占用 CPU 核心
使用场景 通用互斥 中断处理、内核层

附录

A. 编译与运行

makefile 复制代码
# Makefile
CC       = gcc
CFLAGS   = -Wall -Wextra -O2 -g
LDFLAGS  = -lpthread

all: exp1 exp2 exp3

exp1: exp1_thread_basics.c
    $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)

exp2: exp2_thread_advanced.c
    $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)

exp3: exp3_thread_sync.c
    $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)

clean:
    rm -f exp1 exp2 exp3

B. 同步原语速查表

原语 初始化 wait/acquire signal/release 场景
mutex PTHREAD_MUTEX_INITIALIZER pthread_mutex_lock() pthread_mutex_unlock() 临界区保护
cond PTHREAD_COND_INITIALIZER pthread_cond_wait() pthread_cond_signal/broadcast() 条件等待
rwlock PTHREAD_RWLOCK_INITIALIZER rdlock/wrlock unlock 读多写少
semaphore sem_init(&s,0,N) sem_wait() sem_post() 资源池/限流
barrier pthread_barrier_init() pthread_barrier_wait() (内置) 分阶段同步
spinlock pthread_spin_init() pthread_spin_lock() pthread_spin_unlock() 极短临界区

C. 常见陷阱

  1. join vs detach : 创建后必须 join detach,否则线程退出时资源泄漏 (僵尸线程)
  2. while 不是 if : pthread_cond_wait 必须用 while 包围条件检查,防止伪唤醒
  3. 死锁 : 多个线程以不同顺序获取多把锁 → 用 PTHREAD_MUTEX_ERRORCHECK 调试
  4. exit() 在线程中 : 会终止整个进程,不是单线程 → 用 returnpthread_exit()
  5. 取消点未知 : 默认延迟取消只在取消点生效,关键区域先 PTHREAD_CANCEL_DISABLE
  6. TSD key 泄漏 : pthread_key_create 必须配对 pthread_key_delete

D. 环境信息

复制代码
操作系统: Ubuntu 24.04.4 LTS (GNU/Linux 6.8.0-71-generic)
CPU:      FlexusX x2e.8u.16g (8vCPU)
内存:     16 GiB
编译器:   gcc 13.3.0
线程库:   NPTL 2.39 (glibc 内置)
实验节点: ecs-57c4-0004 (120.46.167.216)

文档版本 : v1.0 | 创建日期 : 2026-06-17

运行环境 : ecs-57c4-0004 (华为云香港 FlexusX)

对应实验: 线程基本概念 · 高级话题 · 互斥与同步