操作系统学习(八)——同步
一、同步
在操作系统和并发编程中,同步(Synchronization) 是指协调多个线程或进程之间的执行顺序,以保证程序的正确性与一致性。
同步的核心目的在于解决共享资源访问、线程调度顺序等问题,避免竞态条件(Race Condition)、数据不一致或死锁。
同步 = 保证多个线程/进程按照一定顺序协作执行,共享数据的一致性与完整性得以维护。
举例说明:
- 两个线程同时修改一个全局变量,如果不进行同步,最终结果可能错误。
- 消费者线程不能消费一个还没被生产者生产出来的数据项,这就需要同步。
二、同步 vs 互斥
对比项 | 互斥(Mutual Exclusion) | 同步(Synchronization) |
---|---|---|
目的 | 防止多个线程同时访问共享资源 | 保证多个线程按指定顺序协作执行 |
关键字 | 关键段、互斥锁 | 信号量、条件变量、屏障等 |
是否通信 | 否,侧重排他 | 是,侧重协调和等待/通知 |
示例 | 多线程写同一文件需加锁 | 消费者等待生产者生产数据 |
三、常见同步机制
同步机制 | 描述 |
---|---|
信号量(Semaphore) | 用于控制访问共享资源的数量或线程的同步 |
条件变量(Condition Variable) | 线程等待某个条件成立后被唤醒 |
屏障(Barrier) | 多个线程相互等待,全部达到屏障点再继续 |
事件(Event) | 用于线程之间通知/唤醒 |
原子操作(Atomic) | 无需锁的同步方式(如 std::atomic ) |
四、同步机制详细解析
1. 信号量(Semaphore)
-
是一个计数器变量,用于控制多个线程对资源的访问。
-
常见两种类型:
- 二值信号量(Binary Semaphore):只允许一个线程访问(类似互斥锁);
- 计数信号量(Counting Semaphore):允许多个线程同时访问资源。
POSIX 信号量 API:
函数 | 描述 |
---|---|
sem_init | 初始化信号量 |
sem_wait (P操作) | 等待信号量,值减1 |
sem_post (V操作) | 释放信号量,值加1 |
sem_destroy | 销毁信号量 |
示例1(POSIX
)—— 互斥(代替互斥锁):
sem_t mutex;
sem_init(&mutex, 0, 1); // 初始化为 1(只有1个线程能进入)// 线程代码
sem_wait(&mutex); // 等待进入临界区
// 访问共享资源
sem_post(&mutex); // 离开临界区,释放资源
示例2(POSIX
)—— 线程同步:
示例:一个线程等待另一个线程完成某事。
sem_t ready;
sem_init(&ready, 0, 0);void* producer(void*)
{// 做一些准备工作sem_post(&ready); // 通知消费者
}void* consumer(void*)
{sem_wait(&ready); // 等待生产者通知// 开始消费
}
示例3(POSIX
)—— 资源计数:
控制同一时间最多 N 个线程进入某区域。
sem_t sem;
sem_init(&sem, 0, N); // 最多允许 N 个并发void* worker(void*)
{sem_wait(&sem); // 有“票”才能进入// 执行任务sem_post(&sem); // 释放“票”
}
2. 条件变量(Condition Variable)
- 用于线程等待一个特定条件的发生;
- 常与互斥锁搭配使用。
关键函数接口:
函数 | 作用 |
---|---|
pthread_cond_init() | 初始化条件变量 |
pthread_cond_wait() | 等待条件,阻塞线程 |
pthread_cond_signal() | 唤醒一个等待线程 |
pthread_cond_broadcast() | 唤醒所有等待线程 |
pthread_cond_destroy() | 销毁条件变量 |
示例(POSIX
,生产者-消费者):
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;void* producer(void* arg)
{sleep(1); // 模拟生产时间pthread_mutex_lock(&mutex);ready = 1;printf("生产者:资源已就绪,通知消费者\n");pthread_cond_signal(&cond); // 通知消费者pthread_mutex_unlock(&mutex);return NULL;
}void* consumer(void* arg)
{pthread_mutex_lock(&mutex);while (!ready) {printf("消费者:等待资源...\n");pthread_cond_wait(&cond, &mutex); // 自动解锁并阻塞,直到被唤醒}printf("消费者:收到通知,开始处理资源\n");pthread_mutex_unlock(&mutex);return NULL;
}int main()
{pthread_t prod, cons;pthread_create(&prod, NULL, producer, NULL);pthread_create(&cons, NULL, consumer, NULL);pthread_join(prod, NULL);pthread_join(cons, NULL);pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);return 0;
}
3. 屏障(Barrier)
关键函数接口:
函数名 | 作用 |
---|---|
pthread_barrier_init() | 初始化屏障对象 |
pthread_barrier_wait() | 等待屏障,同步点 |
pthread_barrier_destroy() | 销毁屏障对象 |
- 用于多线程并发场景,强制线程等待,直到全部线程都到达屏障点,才统一继续执行。
示例(POSIX
):
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>#define THREAD_COUNT 3
pthread_barrier_t barrier;void* task(void* arg)
{int id = *(int*)arg;printf("线程 %d 开始第一阶段工作\n", id);sleep(id); // 模拟不同线程执行速度printf("线程 %d 到达屏障,等待其他线程...\n", id);pthread_barrier_wait(&barrier); // 所有线程到达后继续printf("线程 %d 通过屏障,开始第二阶段工作\n", id);return NULL;
}int main() {pthread_t threads[THREAD_COUNT];int ids[THREAD_COUNT];pthread_barrier_init(&barrier, NULL, THREAD_COUNT);for (int i = 0; i < THREAD_COUNT; ++i) {ids[i] = i + 1;pthread_create(&threads[i], NULL, task, &ids[i]);}for (int i = 0; i < THREAD_COUNT; ++i)pthread_join(threads[i], NULL);pthread_barrier_destroy(&barrier);return 0;
}
4. 原子操作(Atomic)
- 使用底层硬件指令支持的原子性操作,无需加锁;
- 适用于简单变量(计数器、自增等)。
示例(C++):
#include <atomic>
std::atomic<int> counter = 0;void thread_func()
{counter++; // 原子自增,无需加锁
}
五、同步的经典问题
1. 生产者-消费者问题
- 问题描述:一个线程生产数据,一个线程消费,必须保证:生产→消费→再生产→…
- 解决方式:信号量 + 条件变量。
2. 哲学家就餐问题
- 问题描述:五位哲学家围坐,每人要用左右筷子才能吃饭,互相抢资源导致死锁。
- 解决方式:通过设定拿筷子的顺序、加入等待/超时机制解决同步问题。
六、同步存在的问题
问题 | 描述 |
---|---|
死锁 | 多个线程相互等待资源,永远阻塞 |
优先级反转 | 高优线程被低优线程阻塞 |
忙等待 | 无效循环等待条件满足,浪费CPU |
性能下降 | 同步过多导致串行执行 |
七、应用场景
场景 | 所需同步机制 |
---|---|
限制并发访问数据库连接池 | 信号量 |
多线程按顺序执行任务 | 条件变量 |
并行计算中的任务阶段同步 | 屏障 |
共享数据的并发计数 | 原子变量 |
八、同步的优化
- 避免过度同步,降低锁粒度,提高并发度;
- 能用原子操作就不用锁;
- 使用线程安全的数据结构(如线程安全队列);
- 尽量避免线程之间复杂依赖,降低死锁概率;
- 使用现代语言提供的高层抽象(如 C++
std::future
, JavaCountDownLatch
)。