C高级:有关读写锁、条件变量与屏障的学习记录
在前面的文章中,我们学习了互斥锁和信号量——它们是线程同步的基础工具。但在实际开发中,仅靠这两者往往不够灵活。Linux 的 pthreads 库还提供了三种更高级的同步机制:读写锁(Read-Write Lock)、条件变量(Condition Variable) 和 屏障(Barrier)。本文将逐一讲解它们的概念、作用和使用方法,最后将互斥锁、信号量、读写锁、条件变量、屏障五种机制做一个全面对比。以下是我在职坐标的学习记录:
一、读写锁(Read-Write Lock)
1.1 什么是读写锁
读写锁是一种特殊的互斥锁,它将锁分为两种模式:读锁(共享锁) 和 写锁(排他锁)。其核心规则是:
- 多个线程可以同时持有读锁(共享模式),只要没有线程持有写锁。
- 写锁是独占的——任何时刻只能有一个线程持有写锁,且持有写锁时不允许其他线程持有读锁。
可以这样理解:读写锁就像一间图书馆的阅览室——多个读者可以同时进去看书(读锁共享),但如果有人要进去装修(写锁),就必须等所有读者都离开,而且装修期间不允许任何人进入。
1.2 读写锁的作用
- 读多写少场景的性能优化:在大量读操作、少量写操作的场景下,读写锁允许多个读者并发访问,性能远优于普通互斥锁(互斥锁在任何时刻都只允许一个线程进入,即使是两个读者也不行)。
- 保护可分离读写的数据结构:如配置信息、缓存、查找表等——这些数据大部分时间只被读取,偶尔被更新。
1.3 读写锁的规则
| 当前状态 | 请求读锁 | 请求写锁 |
|---|---|---|
| 无锁 | ✅ 立即获取 | ✅ 立即获取 |
| 已有读锁(一个或多个) | ✅ 立即获取(共享) | ❌ 阻塞等待所有读锁释放 |
| 已有写锁 | ❌ 阻塞等待写锁释放 | ❌ 阻塞等待写锁释放 |
1.4 相关函数
#include <pthread.h>
| 函数 | 作用 |
|---|---|
pthread_rwlock_init() |
初始化读写锁 |
pthread_rwlock_destroy() |
销毁读写锁 |
pthread_rwlock_rdlock() |
获取读锁(阻塞) |
pthread_rwlock_tryrdlock() |
尝试获取读锁(非阻塞) |
pthread_rwlock_wrlock() |
获取写锁(阻塞) |
pthread_rwlock_trywrlock() |
尝试获取写锁(非阻塞) |
pthread_rwlock_unlock() |
释放读锁或写锁 |
1.5 关键函数详解
初始化
// 静态初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 动态初始化
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
// rwlock:读写锁指针
// attr:属性,通常传 NULL 使用默认属性
加读锁 / 加写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 加读锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 加写锁
// 成功返回 0,失败返回错误码
// 如果无法获取,线程会阻塞等待
解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 无论是读锁还是写锁,都用同一个函数解锁
销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 销毁前必须确保没有线程持有或等待该锁
1.6 完整示例:多线程读写配置
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
// 共享配置数据
char config[256] = "default_config";
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 读者线程:读取配置(多个读者可并发)
void *reader(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < 3; i++) {
pthread_rwlock_rdlock(&rwlock); // 加读锁
printf("[读者 %d] 读取配置: %s\n", id, config);
pthread_rwlock_unlock(&rwlock); // 释放读锁
usleep(100000);
}
return NULL;
}
// 写者线程:更新配置(独占访问)
void *writer(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < 2; i++) {
pthread_rwlock_wrlock(&rwlock); // 加写锁
snprintf(config, sizeof(config), "config_v%d_%d", id, i);
printf("[写者 %d] 更新配置: %s\n", id, config);
pthread_rwlock_unlock(&rwlock); // 释放写锁
usleep(300000); // 写操作较慢
}
return NULL;
}
int main() {
pthread_t readers[3], writers[2];
int r_ids[3] = {1, 2, 3};
int w_ids[2] = {1, 2};
for (int i = 0; i < 3; i++)
pthread_create(&readers[i], NULL, reader, &r_ids[i]);
for (int i = 0; i < 2; i++)
pthread_create(&writers[i], NULL, writer, &w_ids[i]);
for (int i = 0; i < 3; i++)
pthread_join(readers[i], NULL);
for (int i = 0; i < 2; i++)
pthread_join(writers[i], NULL);
pthread_rwlock_destroy(&rwlock);
printf("最终配置: %s\n", config);
return 0;
}
二、条件变量(Condition Variable)
2.1 什么是条件变量
条件变量是一种用于线程间同步协调的机制。它允许一个或多个线程等待某个特定条件成立(如"缓冲区不为空"、"任务队列中有新任务"等)。条件变量本身不保护共享资源,它必须与互斥锁配合使用。
可以这样理解:条件变量就像公司的内部通知系统——员工(线程)在休息室等待(pthread_cond_wait),当有任务发布时(pthread_cond_signal),休息室的某个员工被叫醒去干活;如果是全体通知(pthread_cond_broadcast),所有员工都被叫醒。
2.2 条件变量的作用
- 线程等待条件满足:线程可以在某个条件不满足时进入等待状态,不浪费 CPU 资源(相比忙等待/轮询)。
- 线程间事件通知:一个线程完成某项工作后,通过条件变量通知其他等待的线程。
- 实现生产者-消费者模型:生产者在缓冲区放入数据后通知消费者,消费者在缓冲区为空时等待。
2.3 条件变量与互斥锁的配合
条件变量的使用有一个固定的"套路",必须与互斥锁配合:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int condition = 0; // 共享条件变量
// ---- 等待方(消费者)----
pthread_mutex_lock(&mutex);
while (condition == 0) { // 用 while 而不是 if(防止虚假唤醒)
pthread_cond_wait(&cond, &mutex);
}
// 此时 condition != 0,执行操作
pthread_mutex_unlock(&mutex);
// ---- 通知方(生产者)----
pthread_mutex_lock(&mutex);
condition = 1; // 修改条件
pthread_cond_signal(&cond); // 唤醒一个等待线程
pthread_mutex_unlock(&mutex);
为什么用 while 而不是 if? 因为条件变量存在"虚假唤醒"(spurious wakeup)的可能——线程可能在没有人 signal 的情况下被唤醒。使用 while 循环可以确保每次唤醒后都重新检查条件是否真正满足。
为什么 pthread_cond_wait 需要传入互斥锁? 因为 pthread_cond_wait 内部会做两件事(原子操作):
- 释放互斥锁(让其他线程可以修改条件)。
- 将当前线程放入等待队列并阻塞。
当线程被唤醒时,pthread_cond_wait 返回前会自动重新获取互斥锁。
2.4 相关函数
#include <pthread.h>
| 函数 | 作用 |
|---|---|
pthread_cond_init() |
初始化条件变量 |
pthread_cond_destroy() |
销毁条件变量 |
pthread_cond_wait() |
等待条件成立(阻塞,需配合互斥锁) |
pthread_cond_timedwait() |
带超时的等待 |
pthread_cond_signal() |
唤醒一个等待该条件变量的线程 |
pthread_cond_broadcast() |
唤醒所有等待该条件变量的线程 |
2.5 关键函数详解
初始化
// 静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 动态初始化
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
等待
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
// cond:条件变量指针
// mutex:关联的互斥锁指针(必须是当前线程已持有的锁)
// 行为:原子地释放 mutex 并阻塞等待 cond,被唤醒后重新获取 mutex
唤醒
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒一个等待线程
int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有等待线程
2.6 完整示例:生产者-消费者(条件变量版)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
#define NUM_ITEMS 8
int buffer[BUFFER_SIZE];
int count = 0; // 当前缓冲区中的物品数
int in = 0, out = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER; // 缓冲区不满
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER; // 缓冲区不空
void *producer(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < NUM_ITEMS; i++) {
int item = id * 100 + i;
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) {
printf("[生产者 %d] 缓冲区满,等待...\n", id);
pthread_cond_wait(¬_full, &mutex);
}
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
count++;
printf("[生产者 %d] 生产: %d (count=%d)\n", id, item, count);
pthread_cond_signal(¬_empty); // 通知消费者
pthread_mutex_unlock(&mutex);
usleep(100000);
}
return NULL;
}
void *consumer(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < NUM_ITEMS; i++) {
pthread_mutex_lock(&mutex);
while (count == 0) {
printf("[消费者 %d] 缓冲区空,等待...\n", id);
pthread_cond_wait(¬_empty, &mutex);
}
int item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
printf("[消费者 %d] 消费: %d (count=%d)\n", id, item, count);
pthread_cond_signal(¬_full); // 通知生产者
pthread_mutex_unlock(&mutex);
usleep(200000);
}
return NULL;
}
int main() {
pthread_t prod[2], cons[2];
int p_ids[2] = {1, 2}, c_ids[2] = {1, 2};
for (int i = 0; i < 2; i++)
pthread_create(&prod[i], NULL, producer, &p_ids[i]);
for (int i = 0; i < 2; i++)
pthread_create(&cons[i], NULL, consumer, &c_ids[i]);
for (int i = 0; i < 2; i++) pthread_join(prod[i], NULL);
for (int i = 0; i < 2; i++) pthread_join(cons[i], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
printf("全部完成!\n");
return 0;
}
三、屏障(Barrier)
3.1 什么是屏障
屏障(Barrier)是一种线程汇合同步机制。它让一组线程在某个"屏障点"等待,直到所有线程都到达该点后,才一起继续执行。
可以这样理解:屏障就像马拉松比赛中的集合点——所有选手(线程)从不同路线跑过来,先到的在集合点等待,直到最后一名选手也到达后,大家一起继续前进。
3.2 屏障的作用
- 并行计算的阶段同步:在并行算法中,某些阶段必须等所有线程完成当前步骤后,才能进入下一步。例如矩阵乘法中,所有线程计算完当前行后,才能进入下一轮迭代。
- 多线程初始化同步:多个线程各自完成初始化后,在屏障点汇合,然后一起开始正式工作。
- 迭代算法:如数值模拟、图像处理等需要多轮迭代的算法,每轮迭代结束后需要所有线程同步。
3.3 相关函数
#include <pthread.h>
| 函数 | 作用 |
|---|---|
pthread_barrier_init() |
初始化屏障 |
pthread_barrier_destroy() |
销毁屏障 |
pthread_barrier_wait() |
到达屏障点并等待 |
3.4 关键函数详解
初始化
int pthread_barrier_init(pthread_barrier_t *barrier,
const pthread_barrierattr_t *attr,
unsigned int count);
// barrier:屏障指针
// attr:属性,通常传 NULL
// count:需要等待的线程总数(包括调用 barrier_wait 的所有线程)
等待
int pthread_barrier_wait(pthread_barrier_t *barrier);
// 成功返回:
// - 对于"最后一个到达"的线程:返回 PTHREAD_BARRIER_SERIAL_THREAD(一个正整数)
// - 对于其他线程:返回 0
// 失败返回错误码
销毁
int pthread_barrier_destroy(pthread_barrier_t *barrier);
// 销毁前必须确保没有线程在等待该屏障
3.5 完整示例:并行计算分阶段任务
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#define NUM_THREADS 4
pthread_barrier_t barrier;
int results[NUM_THREADS] = {0};
void *worker(void *arg) {
int id = *(int *)arg;
// ---- 第一阶段:各自独立计算 ----
printf("[线程 %d] 开始第一阶段计算...\n", id);
usleep((id + 1) * 200000); // 模拟不同的计算时间
results[id] = id * 10 + 1;
printf("[线程 %d] 第一阶段完成,结果 = %d\n", id, results[id]);
// ---- 屏障:等待所有线程完成第一阶段 ----
int ret = pthread_barrier_wait(&barrier);
if (ret == PTHREAD_BARRIER_SERIAL_THREAD) {
printf("=== 所有线程已完成第一阶段,进入第二阶段 ===\n");
}
// ---- 第二阶段:读取其他线程的结果继续计算 ----
int sum = 0;
for (int i = 0; i < NUM_THREADS; i++) {
sum += results[i];
}
printf("[线程 %d] 第二阶段:所有结果之和 = %d\n", id, sum);
// ---- 屏障:等待所有线程完成第二阶段 ----
pthread_barrier_wait(&barrier);
if (id == 0) {
printf("=== 所有线程已完成全部计算 ===\n");
}
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
int ids[NUM_THREADS];
// 初始化屏障,count = 线程总数
pthread_barrier_init(&barrier, NULL, NUM_THREADS);
for (int i = 0; i < NUM_THREADS; i++) {
ids[i] = i;
pthread_create(&threads[i], NULL, worker, &ids[i]);
}
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
pthread_barrier_destroy(&barrier);
return 0;
}
运行效果:
[线程 0] 开始第一阶段计算...
[线程 1] 开始第一阶段计算...
[线程 2] 开始第一阶段计算...
[线程 3] 开始第一阶段计算...
[线程 0] 第一阶段完成,结果 = 1
[线程 1] 第一阶段完成,结果 = 11
[线程 2] 第一阶段完成,结果 = 21
[线程 3] 第一阶段完成,结果 = 31
=== 所有线程已完成第一阶段,进入第二阶段 ===
[线程 0] 第二阶段:所有结果之和 = 64
[线程 1] 第二阶段:所有结果之和 = 64
[线程 2] 第二阶段:所有结果之和 = 64
[线程 3] 第二阶段:所有结果之和 = 64
=== 所有线程已完成全部计算 ===
四、五种同步机制的全面对比
4.1 核心特性对比
| 对比维度 | 互斥锁 | 信号量 | 读写锁 | 条件变量 | 屏障 |
|---|---|---|---|---|---|
| 核心用途 | 互斥访问 | 资源计数 + 同步 | 读写分离的互斥 | 条件等待 + 通知 | 多线程汇合 |
| 并发度 | 1 个线程 | N 个线程(初始值决定) | 多个读者 或 1 个写者 | 无(需配合互斥锁) | 所有线程到达后一起放行 |
| 是否有所有权 | ✅ 谁加锁谁解锁 | ❌ 任何线程可 post | ✅ 谁加锁谁解锁 | ❌ 任何线程可 signal | ❌ 自动管理 |
| 能否独立使用 | ✅ | ✅ | ✅ | ❌ 必须配合互斥锁 | ✅ |
| 是否阻塞 | 是(锁被占用时) | 是(计数为 0 时) | 是(冲突时) | 是(条件不满足时) | 是(未全部到达时) |
| 头文件 | <pthread.h> |
<semaphore.h> |
<pthread.h> |
<pthread.h> |
<pthread.h> |
4.2 适用场景对比
| 场景 | 最佳选择 | 说明 |
|---|---|---|
| 保护全局计数器 | 互斥锁 | 简单互斥,性能足够 |
| 保护链表/哈希表(读多写少) | 读写锁 | 多读者并发,性能优于互斥锁 |
| 控制并发连接数(如连接池) | 信号量 | 初始值设为最大连接数 |
| 生产者-消费者模型 | 条件变量 + 互斥锁 | 生产完通知消费者,缓冲空时消费者等待 |
| 线程间事件通知 | 条件变量 | 一方等待条件成立,另一方通知 |
| 并行计算分阶段同步 | 屏障 | 所有线程完成当前阶段后才进入下一阶段 |
| 保护配置文件/缓存 | 读写锁 | 读取频繁、更新稀少 |
| 多线程初始化后统一开始 | 屏障 | 初始化完成后在屏障点汇合 |
4.3 相同点
五种机制都是为了解决多线程并发访问共享资源时的一致性和协调性问题,它们有以下共同点:
- 目的相同:都是为了防止数据竞争、保证程序正确性。
- 都有阻塞能力:当条件不满足时,线程会进入休眠状态,不浪费 CPU 资源。
- 都提供了初始化/销毁接口:使用前需要初始化,使用后需要销毁以释放资源。
- 都需要注意正确使用:如忘记解锁会导致死锁、虚假唤醒需要 while 循环检查等。
- 都属于 POSIX 标准:跨平台兼容性好,Linux/macOS 等系统通用。
4.4 不同点
| 差异点 | 互斥锁 | 信号量 | 读写锁 | 条件变量 | 屏障 |
|---|---|---|---|---|---|
| 状态模型 | 二值(锁/解锁) | 整型计数器 | 三态(无锁/读锁/写锁) | 条件成立/不成立 | 到达/未到达 |
| 能否跨进程 | 可以(配合共享内存) | ✅ 原生支持 | 可以(配合共享内存) | 可以(配合共享内存) | 可以(配合共享内存) |
| 唤醒方式 | 自动(unlock 时) | 自动(post 时) | 自动(unlock 时) | 手动 signal/broadcast | 自动(全部到达时) |
| 能否超时等待 | ❌(有 trylock) | ✅ sem_timedwait |
❌(有 trylock) | ✅ cond_timedwait |
❌ |
| 性能 | 高 | 中 | 读多写少时高 | 取决于场景 | 高 |
| 复杂度 | 低 | 中 | 中 | 高(需配合互斥锁) | 低 |
4.5 如何选择
需要保护共享资源?
├─ 读操作远多于写操作?
│ └─ ✅ 读写锁
├─ 只允许一个线程访问?
│ └─ ✅ 互斥锁
└─ 允许 N 个线程同时访问?
└─ ✅ 信号量(初始值 = N)
需要线程间协调执行顺序?
├─ 一方等待条件成立,另一方通知?
│ └─ ✅ 条件变量 + 互斥锁
└─ 多个线程到达同一点后一起继续?
└─ ✅ 屏障
五、组合使用的经典模式
在实际项目中,这五种机制经常组合使用:
5.1 线程安全队列(互斥锁 + 条件变量)
这是最常用的组合模式,几乎所有线程池、任务队列的底层实现都基于此:
typedef struct {
int data[100];
int head, tail, count;
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;
} ThreadSafeQueue;
void queue_init(ThreadSafeQueue *q) {
q->head = q->tail = q->count = 0;
pthread_mutex_init(&q->mutex, NULL);
pthread_cond_init(&q->not_full, NULL);
pthread_cond_init(&q->not_empty, NULL);
}
void queue_push(ThreadSafeQueue *q, int item) {
pthread_mutex_lock(&q->mutex);
while (q->count == 100)
pthread_cond_wait(&q->not_full, &q->mutex);
q->data[q->tail] = item;
q->tail = (q->tail + 1) % 100;
q->count++;
pthread_cond_signal(&q->not_empty);
pthread_mutex_unlock(&q->mutex);
}
int queue_pop(ThreadSafeQueue *q) {
pthread_mutex_lock(&q->mutex);
while (q->count == 0)
pthread_cond_wait(&q->not_empty, &q->mutex);
int item = q->data[q->head];
q->head = (q->head + 1) % 100;
q->count--;
pthread_cond_signal(&q->not_full);
pthread_mutex_unlock(&q->mutex);
return item;
}
5.2 读写锁 + 条件变量(读写缓存)
// 缓存结构
typedef struct {
char data[1024];
int valid;
pthread_rwlock_t rwlock;
pthread_cond_t cond;
} Cache;
// 读缓存(等待缓存有效)
void cache_read(Cache *c, char *out) {
pthread_rwlock_rdlock(&c->rwlock);
while (!c->valid) {
// 注意:条件变量不能直接配合读写锁使用
// 实际中通常改用互斥锁 + 条件变量
pthread_rwlock_unlock(&c->rwlock);
usleep(10000);
pthread_rwlock_rdlock(&c->rwlock);
}
strcpy(out, c->data);
pthread_rwlock_unlock(&c->rwlock);
}
六、常见陷阱总结
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| 忘记解锁 | 临界区内 return/break 导致死锁 | 使用 RAII 模式或确保所有路径都解锁 |
| 虚假唤醒 | pthread_cond_wait 可能无故返回 |
用 while 循环检查条件 |
| 读写锁饥饿 | 写者一直等不到读锁释放 | 考虑使用写优先的读写锁策略 |
| 屏障计数错误 | count 设置不正确导致永久阻塞 |
确保 count = 实际需要到达的线程数 |
| 信号量溢出 | sem_post 次数超过上限 |
避免无意义的 post |
| 条件变量信号丢失 | signal 时没有线程在等待 | 使用互斥锁保护条件状态,确保 signal 前条件已修改 |
七、总结
| 机制 | 一句话概括 | 最佳场景 |
|---|---|---|
| 互斥锁 | 同一时刻只允许一个线程进入 | 保护共享变量 |
| 信号量 | 控制同时访问的线程数量 | 资源计数、并发限流 |
| 读写锁 | 多读单写,读多写少时性能最优 | 配置信息、缓存 |
| 条件变量 | 等待某个条件成立再继续 | 生产者-消费者、事件通知 |
| 屏障 | 所有线程到达后一起继续 | 并行计算分阶段同步 |
这五种同步机制是 Linux 多线程编程的核心工具。理解它们各自的特点和适用场景,根据实际需求选择最合适的机制(或组合使用),是写出正确、高效并发程序的关键。
浙公网安备 33010602011771号