目录
[条件变量(Condition Variable)](#条件变量(Condition Variable))
[pthread_cond_wait 为什么必须配 mutex?](#pthread_cond_wait 为什么必须配 mutex?)
多线程问题本质是三件事:
- 互斥(Mutual Exclusion)
- 多个线程不能同时访问共享资源
- 典型:修改全局变量、队列、文件
- 同步(Synchronization)
- 线程之间"按顺序发生"
- 典型:A必须先完成,B才能继续
- 通信(Communication)
- 一个线程通知另一个线程"条件已满足"
互斥锁(Mutex)
1. 作用 :保证同一时刻只有一个线程进入临界区
cpp
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
示例代码
cpp
#include <stdio.h>
#include <pthread.h>
int counter = 0;
pthread_mutex_t mutex;
void* add(void* arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mutex);
counter++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, add, NULL);
pthread_create(&t2, NULL, add, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
printf("counter = %d\n", counter);
return 0;
}
如果不加锁,.结果通常 < 200000(丢失更新)
pthread_create
│
├── &t1 → 线程ID输出
├── NULL → 线程属性(默认)
├── add → 线程入口函数
└── NULL → 传给线程的参数
pthread_join(t2, NULL);表示不接受线程的返回值。
信号量(Semaphore)
1. 作用 :信号量 = 计数型资源管理 + 线程同步
类比:
- 10个停车位
- 来一辆车 -1
- 走一辆车 +1
2. 两种类型
(1)二值信号量(类似 mutex)
- 值:0 / 1
- 可实现互斥
(2)计数信号量(更常用)
- 控制资源数量
API(Linux POSIX)
cpp
#include <semaphore.h>
sem_t sem;
sem_init(&sem, 0, value); // value 初始值
sem_wait(&sem); // P操作:-1(阻塞)
sem_post(&sem); // V操作:+1
sem_destroy(&sem);
//sem_wait() 会尝试把信号量减 1
如果当前值为 0,则不会失败返回,而是阻塞等待,直到信号量变成 >0(sem_post)
sem_init 的 value 表示信号量初始可用资源数量,
pshared=0 表示线程间共享(同一个进程),如果不等于0 表示进程间共享内存。
示例:限制最多3个线程同时访问资源
cpp
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t sem;
void* worker(void* arg) {
int id = *(int*)arg;
//void *temp 就是:一个"类型未知的指针变量 temp",可以临时指向任何类型的数据,但使用前必须转换类型。
sem_wait(&sem); // 进入资源区(-1)
printf("Thread %d is working...\n", id);
sleep(2);
printf("Thread %d done\n", id);
sem_post(&sem); // 释放资源(+1)
return NULL;
}
int main() {
pthread_t t[10];
int id[10];
sem_init(&sem, 0, 3); // 最多3个线程同时运行
for (int i = 0; i < 10; i++) {
id[i] = i;
pthread_create(&t[i], NULL, worker, &id[i]);
}
for (int i = 0; i < 10; i++) {
pthread_join(t[i], NULL);
}
sem_destroy(&sem);
return 0;
}
解释:
这段程序通过信号量 sem 控制线程并发数量。初始化时将信号量设为 3,表示同一时刻最多允许 3 个线程进入临界区执行任务。
程序创建了 10 个线程,每个线程在进入"工作区"之前都会调用 sem_wait() 申请资源。如果当前信号量大于 0,线程可以继续执行;如果已经为 0,则必须等待其他线程释放资源。
进入临界区后,线程打印信息并 sleep 2 秒模拟工作,完成后调用 sem_post() 释放资源,使其他等待线程可以继续进入。
存在问题;
多个线程是同时运行的 ,但 i 是在循环里变化的。
所以可能出现这种情况:
- 线程A还没来得及读
id[i] - 主线程已经把
i改成下一个值了
结果:线程可能拿到"错的 id"
条件变量(Condition Variable)
举个例子:
你在等快递:
- 你先放下手里的钥匙(unlock)
- 去沙发睡觉
- 快递员来了叫你
- 你醒来再拿回钥匙(lock)
1. 作用
用于"线程等待某个条件成立" 不是互斥,而是:
线程睡眠 + 被通知唤醒
2. 必须搭配 mutex 使用
cpp
pthread_mutex_t mutex;
pthread_cond_t cond;
3. 核心API
cpp
pthread_cond_wait(&cond, &mutex);
pthread_cond_signal(&cond);
pthread_cond_broadcast(&cond);
/*wait = 解锁睡觉
signal = 叫醒一个
broadcast = 叫醒全部*/
cond_wait 不是单纯"睡觉"
它其实是:
先解锁 mutex + 再睡觉 + 被叫醒后再加锁"
wait = 解锁 + 睡觉 + 被唤醒 + 再加锁
为什么一定要这样设计?
因为如果它不先解锁:
别的线程就永远进不来修改条件, 那你就永远醒不过来(死锁)
4.示例代码
cpp
#include <stdio.h>
#include <pthread.h>
int data_ready = 0;
int data = 0;
pthread_mutex_t mutex;
pthread_cond_t cond;
void* producer(void* arg) {
for (int i = 1; i <= 5; i++) {
pthread_mutex_lock(&mutex);
data = i;
data_ready = 1;
printf("Producer produced: %d\n", data);
pthread_cond_signal(&cond); // 通知消费者
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 1; i <= 5; i++) {
pthread_mutex_lock(&mutex);
while (data_ready == 0) {
pthread_cond_wait(&cond, &mutex);
}
printf("Consumer consumed: %d\n", data);
data_ready = 0;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t p, c;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_create(&p, NULL, producer, NULL);
pthread_create(&c, NULL, consumer, NULL);
pthread_join(p, NULL);
pthread_join(c, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
consumer 被唤醒后,必须等 producer 释放 mutex,然后再去抢锁(竞争 mutex)
五、pthread_cond_wait 为什么必须配 mutex?
这是重点面试题:
原子(atomic)在计算机里的意思,不是物理里的"不可分割的原子",而是一个非常重要的并发概念:
原子操作 = 要么完全执行完成,要么完全不执行,中间不会被打断,也不会出现"执行一半"的状态。
它做了三件事(原子操作):
-
解锁 mutex
-
线程进入睡眠
-
被唤醒后重新加锁 mutex