线程同步
线程同步指的是当一个线程在对某个临界资源进行操作时,其他线程都不可以对这个资源进行操作,直到该线程完成操作,其他线程才能操作,也就是协同步调,让线程按预定的先后次序进行运行。线程同步的方法有四种:
互斥锁、信号量、条件变量、读写锁
互斥锁
用于保护临界区,确保同一时间只有一个线程可以访问共享资源。
互斥锁的创建和初始化
方法一:静态初始化(推荐)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
方法二:动态初始化
cpp
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); // 第二个参数为属性,NULL表示默认
// 使用后必须销毁
pthread_mutex_destroy(&mutex);
互斥锁函数总结表格(带实例)
|------------|--------------------------------------------------------------------------------------------------------------------------|---------------|-------------------|
| 函数类别 | 函数原型 | 作用 | 返回值 |
| 初始化 | pthread_mutex_init(&mutex, NULL); pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; | 初始化互斥锁 静态初始化 | 0=成功 |
| 销毁 | pthread_mutex_destroy(&mutex); | 销毁互斥锁,释放资源 | 0=成功 |
| 阻塞加锁 | pthread_mutex_lock(&mutex); // 临界区代码 pthread_mutex_unlock(&mutex); | 阻塞式加锁,等待直到获取锁 | 0=成功 |
| 非阻塞加锁 | if (pthread_mutex_trylock(&mutex) == 0) { // 成功获取锁 pthread_mutex_unlock(&mutex); } else { // 锁正忙 } | 尝试加锁,立即返回不阻塞 | 0=成功 EBUSY=锁忙 |
| 超时加锁 | struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 2; // 2秒超时 pthread_mutex_timedlock(&mutex, &ts); | 在指定时间内等待锁 | 0=成功 ETIMEDOUT=超时 |
| 解锁 | pthread_mutex_unlock(&mutex); | 释放互斥锁 | 0=成功 |
| 属性初始化 | pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_destroy(&attr); | 初始化/销毁属性对象 | 0=成功 |
| 设置锁类型 | pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&mutex, &attr); | 设置递归锁、错误检查锁等 | 0=成功 |
| 获取锁类型 | int type; pthread_mutexattr_gettype(&attr, &type); | 获取当前锁类型 | 0=成功 |
| 设置进程共享 | pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); | 设置进程间共享 | 0=成功 |
cpp
#include <stdio.h>
#include <pthread.h>
// 全局变量和锁
int counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* increment(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_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("最终结果: %d (应该是200000)\n", counter);
return 0;
}
互斥锁的两种类型
不可重入锁(默认的 pthread_mutex_t**)**
- 特点 :同一线程不能对同一把锁加锁两次。
- 后果 :第二次调用 pthread_mutex_lock 会死锁(自己把自己卡住)。
cpp
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void foo() {
pthread_mutex_lock(&lock); // 第一次加锁 OK
printf("第一次加锁成功\n");
pthread_mutex_lock(&lock); // 第二次加锁 -> 死锁!
printf("第二次加锁成功\n"); // 这行永远执行不到
pthread_mutex_unlock(&lock);
pthread_mutex_unlock(&lock);
}
int main() {
foo();
return 0;
}
打印 "第一次加锁成功" 后程序卡死,因为第二次加锁时,线程已经持有锁,但又去申请同一把锁,就永远等不到自己释放锁。
可重入锁( PTHREAD_MUTEX_RECURSIVE**)**
- 特点:同一线程可以多次加锁同一把锁,不会死锁。
- 条件:加锁多少次,就要解锁多少次,否则其他线程无法获得锁。
cpp
pthread_mutexattr_t attr;
pthread_mutex_t lock;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
cpp
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t lock;
void foo() {
pthread_mutex_lock(&lock);
printf("第一次加锁\n");
pthread_mutex_lock(&lock);
printf("第二次加锁\n");
pthread_mutex_unlock(&lock);
pthread_mutex_unlock(&lock);
}
int main() {
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
foo();
pthread_mutex_destroy(&lock);
pthread_mutexattr_destroy(&attr);
return 0;
}
第一次加锁
第二次加锁
程序正常结束,因为可重入锁允许同一线程多次加锁。
死锁的原因
不可重入锁死锁的原因是:
- 互斥锁的设计目的就是独占------ 同一把锁在释放前只能被一个线程持有。
- 如果同一线程再次加锁,就相当于 "自己等自己释放锁",永远等不到,于是死锁。
什么时候会不小心锁两次?
- 递归函数调用(外层已经加锁,内层又加锁)。
- 不同函数之间调用,没有意识到上一层已经加锁。
死锁之后,再 unlock 两次也没用 。
线程被挂起,后面的两个 unlock 根本没有机会运行,所以死锁无法解除。
死锁一旦发生,单靠用户代码是解不开的,因为线程已经没法继续执行了。常见的处理方式是:
- 预防:在设计代码时避免出现死锁(比如避免同一线程重复加锁、保证加锁顺序一致等)。
- 调试 :用工具(如 gdb、pstack、htop)确认死锁位置,然后修改代码逻辑。
- 强制结束 :在外部 kill 掉卡死的进程(这是最后的无奈之举)。
如果你用的是可重入锁 (PTHREAD_MUTEX_RECURSIVE):
- 同一线程可以多次 lock,不会死锁。
- 但你必须 unlock 同样的次数,其他线程才能获得这把锁。
- 如果 unlock****次数多于 lock****次数,会导致未定义行为(可能程序崩溃)。