Linux 线程同步:锁的应用总结
一、互斥锁(Mutex)
1. 基本概念
- 作用:确保同一时间只有一个线程访问共享资源,实现互斥访问。
- 核心特性:原子性、阻塞式加锁(未获取锁时线程进入睡眠)。
- 适用场景:保护临界区(如共享变量、文件操作、链表操作等)。
2. 关键 API
函数原型 | 说明 |
---|
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); | 初始化互斥锁,attr 可设为 NULL (默认属性)。 |
int pthread_mutex_lock(pthread_mutex_t *mutex); | 加锁:若锁被占用,线程阻塞等待。 |
int pthread_mutex_trylock(pthread_mutex_t *mutex); | 尝试加锁:若锁可用则获取,否则返回 EBUSY (非阻塞)。 |
int pthread_mutex_unlock(pthread_mutex_t *mutex); | 解锁:释放锁,唤醒等待线程。 |
3. 示例代码
#include <pthread.h>
pthread_mutex_t mutex;
int shared_data = 0;
void* thread_func(void* arg) {
for (int i = 0; i < 1000; i++) {
pthread_mutex_lock(&mutex);
shared_data++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
4. 注意事项
- 死锁风险:避免多个线程以不同顺序获取多个互斥锁(可通过固定加锁顺序解决)。
- 性能开销:锁竞争激烈时,线程上下文切换会带来较大开销。
- 必须配对调用:
lock
与 unlock
需一一对应,否则导致资源泄漏。
二、读写锁(Read-Write Lock)
1. 基本概念
- 作用:允许多个读线程同时访问共享资源,但写线程独占资源,实现“读共享、写独占”。
- 核心特性:读锁共享(
SHARED
)、写锁独占(EXCLUSIVE
),适合读多写少场景。 - 适用场景:配置文件读取、缓存数据访问(读操作远多于写操作)。
2. 关键 API
函数原型 | 说明 |
---|
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); | 初始化读写锁。 |
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); | 加读锁:允许其他读线程同时获取。 |
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); | 加写锁:阻塞所有读/写线程,直至锁释放。 |
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); | 解锁:释放读锁或写锁。 |
3. 示例代码
pthread_rwlock_t rwlock;
int config_data;
void* read_thread(void* arg) {
pthread_rwlock_rdlock(&rwlock);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
void* write_thread(void* arg) {
pthread_rwlock_wrlock(&rwlock);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
4. 注意事项
- 写锁优先级:某些实现中,写锁可能饥饿读线程,需通过属性配置调整优先级。
- 避免长时间持有写锁:写锁独占资源,长时间持有会降低并发性。
- 锁类型匹配:读锁和写锁不能交叉释放(如对读锁调用
wrlock
会导致未定义行为)。
三、自旋锁(Spinlock)
1. 基本概念
- 作用:获取锁时若锁被占用,线程通过循环(自旋)等待,而非进入睡眠。
- 核心特性:非阻塞式加锁,适合锁持有时间极短的场景(避免上下文切换开销)。
- 适用场景:内核态驱动开发、用户态高频短临界区(如无系统调用的纯计算逻辑)。
2. 关键 API
函数原型 | 说明 |
---|
int pthread_spin_init(pthread_spinlock_t *lock, int pshared); | 初始化自旋锁,pshared 设为 PTHREAD_PROCESS_PRIVATE (进程内共享)。 |
int pthread_spin_lock(pthread_spinlock_t *lock); | 加锁:自旋等待直至获取锁。 |
int pthread_spin_trylock(pthread_spinlock_t *lock); | 尝试加锁:若锁可用则获取,否则返回 EBUSY (非阻塞)。 |
int pthread_spin_unlock(pthread_spinlock_t *lock); | 解锁:释放锁,唤醒自旋的线程(实际通过内存屏障通知)。 |
3. 示例代码
pthread_spinlock_t spinlock;
int counter = 0;
void* thread_func(void* arg) {
for (int i = 0; i < 1000; i++) {
pthread_spin_lock(&spinlock);
counter++;
pthread_spin_unlock(&spinlock);
}
return NULL;
}
int main() {
pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);
pthread_spin_destroy(&spinlock);
return 0;
}
4. 注意事项
- 忙等待开销:自旋期间占用 CPU 资源,锁持有时间长会导致 CPU 利用率下降。
- 适用场景限制:仅适用于临界区执行时间极短(纳秒/微秒级)的场景。
- 内存屏障:自旋锁通常依赖硬件原子操作和内存屏障保证数据一致性。
四、递归锁(Recursive Mutex)
1. 基本概念
- 作用:允许同一线程多次获取同一把锁(普通互斥锁会导致死锁),用于递归函数或嵌套加锁场景。
- 核心特性:记录锁的持有次数,同一线程多次加锁时计数器递增,解锁时递减,直至为零释放锁。
- 适用场景:递归调用的函数、同一线程内多次加锁的逻辑(如类的成员函数多次调用自身)。
2. 关键 API
函数原型 | 说明 |
---|
初始化时设置属性:
pthread_mutexattr_t attr;
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); | 通过属性将互斥锁设置为递归锁。 |
3. 示例代码
pthread_mutex_t recursive_mutex;
void recursive_function(int depth) {
pthread_mutex_lock(&recursive_mutex);
if (depth > 0) {
recursive_function(depth - 1);
}
pthread_mutex_unlock(&recursive_mutex);
}
int main() {
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&recursive_mutex, &attr);
return 0;
}
4. 注意事项
- 性能略低:因需维护锁的持有计数,开销比普通互斥锁稍高。
- 避免滥用:仅在确实需要同一线程多次加锁时使用,普通场景优先用非递归锁。
五、条件变量(Condition Variable)
1. 基本概念
- 作用:与互斥锁配合使用,实现线程间的同步(如等待某个条件满足后再执行)。
- 核心特性:允许线程等待特定条件(如“数据准备就绪”“任务队列非空”),避免忙等待。
- 适用场景:生产者-消费者模型、事件通知(一个线程通知多个线程执行)。
2. 关键 API
函数原型 | 说明 |
---|
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); | 等待条件:释放 mutex 并阻塞,条件满足时被唤醒并重新加锁。 |
int pthread_cond_signal(pthread_cond_t *cond); | 唤醒一个等待该条件的线程(单播)。 |
int pthread_cond_broadcast(pthread_cond_t *cond); | 唤醒所有等待该条件的线程(广播)。 |
3. 示例代码(生产者-消费者模型)
pthread_mutex_t mutex;
pthread_cond_t cond;
int buffer[10], count = 0;
void* producer(void* arg) {
for (int i = 0; i < 100; i++) {
pthread_mutex_lock(&mutex);
buffer[count++] = i;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < 100; i++) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(&cond, &mutex);
}
int data = buffer[--count];
pthread_mutex_unlock(&mutex);
}
return NULL;
}
4. 注意事项
- 必须配合互斥锁:
pthread_cond_wait
需在持有锁的情况下调用,确保条件检查的原子性。 - 虚假唤醒:即使没有
signal
/broadcast
,cond_wait
也可能返回,需用 while
循环检查条件(而非 if
)。 - 广播与单播选择:
broadcast
用于唤醒所有等待线程(如资源重置),signal
用于唤醒单个线程(如任务就绪)。
六、屏障(Barrier)
1. 基本概念
- 作用:同步多个线程,确保所有线程到达屏障点后再继续执行(类似“集合点”)。
- 核心特性:等待指定数量的线程全部到达后,统一释放所有线程。
- 适用场景:并行计算中的多阶段同步(如所有线程完成初始化后再进入计算阶段)。
2. 关键 API
函数原型 | 说明 |
---|
int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count); | 初始化屏障,count 为需等待的线程数。 |
int pthread_barrier_wait(pthread_barrier_t *barrier); | 线程到达屏障点,等待其他线程。若为最后一个到达的线程,唤醒所有线程。 |
3. 示例代码
pthread_barrier_t barrier;
void* thread_func(void* arg) {
printf("Thread %d ready\n", *(int*)arg);
pthread_barrier_wait(&barrier);
printf("Thread %d starts working\n", *(int*)arg);
return NULL;
}
int main() {
const int thread_count = 5;
pthread_barrier_init(&barrier, NULL, thread_count);
pthread_barrier_destroy(&barrier);
return 0;
}
4. 注意事项
- 固定线程数:初始化时需指定确切的线程数,无法动态调整。
- 错误处理:
pthread_barrier_wait
对最后一个到达的线程返回 PTHREAD_BARRIER_SERIAL_THREAD
,其他线程返回 0
,可用于区分主线程逻辑。
七、锁的对比与选择
锁类型 | 互斥锁 | 读写锁 | 自旋锁 | 递归锁 | 条件变量 | 屏障 |
---|
加锁方式 | 阻塞式 | 读共享/写独占 | 自旋等待 | 可重入 | 配合互斥锁 | 集合点同步 |
适用场景 | 通用互斥 | 读多写少 | 短临界区 | 递归/嵌套加锁 | 事件通知 | 多线程同步 |
上下文切换 | 有 | 有 | 无(忙等待) | 有 | 有 | 有 |
典型场景 | 共享变量 | 配置文件 | 高频小临界区 | 递归函数 | 生产者-消费者 | 并行计算阶段 |
API 复杂度 | 低 | 中 | 低 | 中 | 中 | 低 |
八、最佳实践
- 减少锁持有时间:将非必要操作移到临界区外,降低锁竞争概率。
- 避免嵌套锁:确保持有锁的顺序一致(如按资源地址升序加锁),防止死锁。
- 选择合适的锁类型:读多写少用读写锁,短临界区用自旋锁,递归逻辑用递归锁。
- 错误处理:检查锁函数的返回值(如
pthread_mutex_lock
返回 EBUSY
表示加锁失败)。 - 性能测试:使用
perf
等工具分析锁竞争热点,优化临界区逻辑。
通过合理选择和使用锁机制,可以在保证线程安全的同时,最大限度提升程序的并发性能。实际开发中需结合具体场景,平衡安全性与效率。