Linux 多线程编程基础入门
集群 : ecs-57c4-0004 (华为云香港 FlexusX, 8vCPU/16GiB, Ubuntu 24.04, gcc 13.3.0)
文档版本 : v1.0 | 创建日期 : 2026-06-17
涵盖实验: 线程基本概念 · 线程高级话题 · 线程互斥与同步
目录
- 1.1 线程 vs 进程
- 1.2 pthread_create / pthread_join
- 1.3 线程终止:return vs pthread_exit
- 1.4 pthread_detach:分离线程
- 1.5 线程栈空间
- 2.1 pthread_attr_t:线程属性
- 2.2 pthread_once:一次性初始化
- 2.3 线程特有数据 (TSD)
- 2.4 pthread_cancel:线程取消
- 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()返回的是库级标识,不同于内核 TIDpthread_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. 常见陷阱
- join vs detach : 创建后必须 join 或 detach,否则线程退出时资源泄漏 (僵尸线程)
while不是if:pthread_cond_wait必须用while包围条件检查,防止伪唤醒- 死锁 : 多个线程以不同顺序获取多把锁 → 用
PTHREAD_MUTEX_ERRORCHECK调试 exit()在线程中 : 会终止整个进程,不是单线程 → 用return或pthread_exit()- 取消点未知 : 默认延迟取消只在取消点生效,关键区域先
PTHREAD_CANCEL_DISABLE - 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)
对应实验: 线程基本概念 · 高级话题 · 互斥与同步