1、线程间通信
1.1 实现方式
- 核心基础: 一个进程空间内部所有线程共享数据段、堆区、全局变量、静态变量、文件描述符,可以利用这些空间进行线程通信
- 问题: 多线程同时操作共享资源(如全局变量空间)时会引发资源竞争,导致数据不一致
- **解决资源竞争的核心思路:**多线程要避免引入资源竞争可以通过加互斥锁解决,保证资源的"互斥访问"
- **常用实现方法:**线程间通信最简单的方法:全局变量+互斥锁
1.2 原子操作
- **定义:**不会被CPU任务调度打断的一次最小的操作称为原子操作,要么完全执行完毕,要么完全不执行,不存在 "执行一半被打断" 的中间状态。
- 应用场景: 适用于简单的数值操作(如计数器、标志位),替代互斥锁以提升性能(如
atomic_int类型的自增 / 自减)。
1.3 互斥锁
- **核心作用:**保证共享资源的 "互斥访问",避免多线程资源竞争。配合资源使用,使用资源前加锁,使用资源结束后解锁;加锁后,无法再次加锁,必须等到解锁后才能继续加锁
- 核心规则:
- 加锁后,其他线程再尝试加锁会阻塞,直到持有锁的线程解锁
- 同一线程不能对已加锁的互斥锁重复加锁(会导致死锁)
- 解锁必须由持有锁的线程执行(其他线程解锁会报错)
- 加锁后必须解锁(否则会导致其他线程永久阻塞)
1.4 互斥锁函数接口(POSIX 标准,Linux 下)
所有 pthread 系列锁函数的通用规则:成功返回 0,失败返回错误码(非 errno,需直接判断返回值,而非通过 perror 获取)。
1.4.1 pthread_mutex_init(互斥锁初始化)
- 函数原型:
cpp
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 功能:初始化一个互斥锁对象,为互斥锁分配必要的资源并设置初始状态(未加锁)。
- 参数:
mutex:指向pthread_mutex_t类型互斥锁对象的首地址,是要初始化的锁本身;attr:互斥锁的属性设置,常用取值为NULL(使用默认属性,即 "普通互斥锁",不可递归加锁、解锁者必须是加锁者);若需自定义属性(如递归锁),需先初始化pthread_mutexattr_t对象。
- 返回值:成功返回 0;失败返回非 0 错误码(如 EINVAL 表示参数无效)。
- 注意事项:
- 互斥锁有两种初始化方式:此函数为 "动态初始化",静态初始化可直接赋值
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;; - 已初始化的锁不能重复初始化,否则会导致未定义行为。
- 互斥锁有两种初始化方式:此函数为 "动态初始化",静态初始化可直接赋值
1.4.2 pthread_mutex_destroy(互斥锁销毁)
-
函数原型:
cppint pthread_mutex_destroy(pthread_mutex_t *mutex); -
功能:销毁已初始化的互斥锁,释放锁占用的系统资源。
-
参数说明:
mutex:指向已初始化的pthread_mutex_t类型互斥锁对象的首地址。
-
返回值:成功返回 0;失败返回非 0 错误码(如 EBUSY 表示锁仍处于加锁状态,EINVAL 表示锁未初始化)。
-
注意事项:
- 销毁前必须确保锁处于解锁状态,否则会报错且可能导致资源泄漏;
- 销毁后的锁不可再使用,除非重新初始化;
- 静态初始化的锁(用
PTHREAD_MUTEX_INITIALIZER)也建议调用此函数销毁,保证兼容性。
1.4.3 pthread_mutex_lock(互斥锁阻塞加锁)
-
函数原型:
cppint pthread_mutex_lock(pthread_mutex_t *mutex); -
功能:尝试为互斥锁加锁,是 "阻塞式" 加锁方式。
-
参数说明:
mutex:指向已初始化的pthread_mutex_t类型互斥锁对象的首地址。
-
行为逻辑:
- 若锁当前未被占用,加锁成功,函数立即返回;
- 若锁已被其他线程占用,当前线程会进入阻塞状态(放弃 CPU 执行权),直到锁被解锁后,竞争到锁资源才会继续执行;
- 若当前线程已持有该锁(非递归锁),会导致死锁(线程自己阻塞自己)。
-
返回值:成功返回 0;失败返回非 0 错误码(如 EINVAL 表示锁未初始化,EDEADLK 表示检测到死锁)。
1.4.4 pthread_mutex_trylock(互斥锁非阻塞加锁)
-
函数原型:
cppint pthread_mutex_trylock(pthread_mutex_t *mutex); -
功能:尝试为互斥锁加锁,是 "非阻塞式" 加锁方式。
-
参数说明:
mutex:指向已初始化的pthread_mutex_t类型互斥锁对象的首地址。
-
行为逻辑:
- 若锁当前未被占用,加锁成功,函数立即返回 0;
- 若锁已被占用(无论被哪个线程持有),函数不阻塞,直接返回错误码 EBUSY,当前线程可继续执行其他逻辑;
- 核心作用:避免线程因加锁陷入永久阻塞,是规避死锁的重要手段。
-
返回值:成功返回 0;失败返回非 0 错误码(EBUSY 表示锁被占用,EINVAL 表示锁未初始化)。
1.4.5 pthread_mutex_unlock(互斥锁解锁)
-
函数原型:
cppint pthread_mutex_unlock(pthread_mutex_t *mutex); -
功能:释放已持有的互斥锁,使锁回到未加锁状态。
-
参数说明:
mutex:指向已初始化且处于加锁状态的pthread_mutex_t类型互斥锁对象的首地址。
-
行为逻辑:
- 解锁成功后,会唤醒所有因该锁阻塞的线程,这些线程会竞争锁资源(由系统调度决定哪个线程获得锁);
- 仅能由持有该锁的线程调用解锁,若其他线程尝试解锁,会导致未定义行为(可能报错 EPERM,或直接崩溃)。
-
返回值:成功返回 0;失败返回非 0 错误码(如 EPERM 表示当前线程未持有该锁,EINVAL 表示锁未初始化)。
-
注意事项:
- 加锁和解锁必须成对出现,漏解锁会导致其他线程永久阻塞;
- 解锁操作应放在临界区结束后,且尽可能靠近临界区,减少锁的持有时间。
1.5 临界代码与临界区
- **定义:**加锁和解锁之间的代码称为临界区或临界代码,这段代码操作共享资源,且同一时间只能被一个线程执行。
- 核心原则: 临界区的范围应尽可能小(仅包含操作共享资源的必要代码),减少线程阻塞时间,提升并发效率。
- 示例:
cpp
pthread_mutex_lock(&mutex); // 加锁
// 临界区:仅包含操作共享资源的代码
global_count++; // 共享资源操作
pthread_mutex_unlock(&mutex); // 解锁
1.6 死锁
- **定义:**多线程因互相持有对方需要的锁,且都不释放自己的锁,导致所有线程永久阻塞、无法继续执行的状态。(多任务通信过程中由于加锁导致多个任务均无法向下执行的状态称为死锁状态,简称为死锁)
- 死锁产生的4个必要条件 (缺一不可):
- 互斥条件:资源只能被一个线程持有(如互斥锁)
- 不可剥夺条件:资源不能被强制夺走,只能由持有线程主动释放
- 请求保持条件:线程持有一个锁的同时,又请求获取另一个锁
- 循环等待条件:线程 A 持有锁 1 并请求锁 2,线程 B 持有锁 2 并请求锁 1,形成循环依赖。
- 死锁的规避方法 (按优先级排序):
- 核心原则:避免嵌套加锁(能不加锁就不加,能少加锁就少加)
- 基础方法:所有线程按相同顺序加锁(如先加锁 A,再加锁 B,所有线程都遵守此顺序)
- 非阻塞加锁:用
pthread_mutex_trylock替代pthread_mutex_lock,加锁失败时完成异常处理流程,主动释放已持有的锁,避免阻塞(程序卡死) - 其他原则:锁的持有时间尽可能短(临界区代码极简)、设置锁的超时时间(避免永久阻塞)
1.7 信号量
信号量是一种同步原语,既可以实现多线程 / 进程间的同步,让多个任务具有先后顺序关系;也可以实现互斥(控制资源访问数量),功能比互斥锁更灵活。
- 同步:多个任务执行有严格的先后逻辑顺序
- 异步:多个任务的执行流程无任何关联,各自独立运行
- 信号量本质:一个 "计数器 + 等待队列",计数器表示可用资源的数量,等待队列存放因资源不足阻塞的线程。(是一个资源,资源可以初始化、销毁、申请和释放)
- 核心规则:
- 申请资源(P 操作,对应 sem_wait) :
- 若信号量计数器 > 0:计数器减 1,申请成功,线程继续执行;
- 若信号量计数器 = 0:线程阻塞,加入等待队列,直到有其他线程释放资源(计数器 > 0);
- 释放资源(V 操作,对应 sem_post) :
- 计数器加 1,释放成功(永远不会阻塞);
- 若等待队列中有阻塞的线程,会唤醒其中一个线程,让其完成资源申请。
- 申请资源(P 操作,对应 sem_wait) :
- 常见应用场景:
- 同步:控制线程执行顺序(如 A 线程执行完后,B 线程才能执行);
- 互斥:当信号量初始值为 1 时(二值信号量),等价于互斥锁;
- 资源限流:当信号量初始值为 N 时,可限制同时访问某资源的线程数为 N(如最多 5 个线程同时读写文件)。
1.8 信号量的函数接口(POSIX 标准,Linux 下)
所有 sem 系列函数通用规则:成功返回 0,失败返回 - 1,且设置 errno(可通过 perror/strerror 查看错误原因)。
1.8.1 sem_init(信号量初始化)
-
函数原型:
cppint sem_init(sem_t *sem, int pshared, unsigned int value); -
功能:初始化一个信号量对象,设置信号量的共享属性和初始计数器值。
-
参数说明:
sem:指向sem_t类型信号量对象的首地址,是要初始化的信号量本身;pshared:信号量的共享范围:0:信号量仅在当前进程的线程间共享(最常用,多线程同步场景);非0:信号量可在多个进程间共享(需配合共享内存使用);
value:信号量的初始计数器值(≥0):- 初始值为 1:二值信号量(等价于互斥锁);
- 初始值为 N(N>1):计数信号量(限流 N 个线程访问资源);
- 初始值为 0:常用于同步(让线程先阻塞,等待其他线程释放资源)。
-
返回值:成功返回 0;失败返回 - 1,errno 被设置(如 EINVAL 表示 value 超出范围,ENOMEM 表示内存不足)。
-
注意事项:
- 已初始化的信号量不能重复初始化,否则会导致未定义行为;
- 线程间同步场景下,
pshared必须设为 0,否则可能无法正常工作。
1.8.2 sem_destroy(信号量销毁)
-
函数原型:
cppint sem_destroy(sem_t *sem); -
功能:销毁已初始化的信号量,释放其占用的系统资源。
-
参数说明:
sem:指向已初始化的sem_t类型信号量对象的首地址。
-
返回值:成功返回 0;失败返回 - 1,errno 被设置(如 EINVAL 表示信号量未初始化,EBUSY 表示仍有线程等待该信号量)。
-
注意事项:
- 销毁前需确保没有线程正在等待该信号量,否则会报错;
- 销毁后的信号量不可再使用,除非重新初始化。
1.8.3 sem_wait(信号量阻塞申请资源 / P 操作)
-
函数原型:
cppint sem_wait(sem_t *sem); -
功能:阻塞式申请信号量资源(P 操作),是信号量的核心 "减计数" 操作。
-
参数说明:
sem:指向已初始化的sem_t类型信号量对象的首地址。
-
行为逻辑:
- 若信号量计数器 > 0:计数器减 1,函数立即返回(申请成功);
- 若信号量计数器 = 0:当前线程进入阻塞状态 (放弃 CPU 执行权),直到有其他线程调用
sem_post释放资源(计数器 > 0),才会唤醒并完成减 1 操作; - 支持 "惊群效应":若多个线程阻塞等待同一个信号量,
sem_post会唤醒其中一个线程(由系统调度决定)。
-
返回值:成功返回 0;失败返回 - 1,errno 被设置(如 EINVAL 表示信号量未初始化,EINTR 表示阻塞时被信号中断)。
-
补充:非阻塞版申请函数
sem_trywait:- 原型:
int sem_trywait(sem_t *sem); - 逻辑:计数器为 0 时不阻塞,直接返回 - 1,errno 设为 EAGAIN,适用于规避阻塞的场景。
- 原型:
1.8.4 sem_post(信号量释放资源 / V 操作)
-
函数原型:
cppint sem_post(sem_t *sem); -
功能:释放信号量资源(V 操作),是信号量的核心 "加计数" 操作。
-
参数说明:
sem:指向已初始化的sem_t类型信号量对象的首地址。
-
行为逻辑:
- 信号量计数器加 1,函数立即返回(永远不会阻塞);
- 若有线程因
sem_wait阻塞等待该信号量,会唤醒其中一个线程,让其完成sem_wait的减计数操作; - 计数器无上限,多次调用会让计数器持续增加(需自行控制,避免资源数溢出)。
-
返回值:成功返回 0;失败返回 - 1,errno 被设置(如 EINVAL 表示信号量未初始化)。
-
注意事项:
sem_post和sem_wait需成对调用,避免计数器异常(如过度释放导致资源数远超预期);- 释放操作可由任意线程执行(无需和申请线程一致),这是和互斥锁的核心区别(互斥锁解锁者必须是加锁者)。
1.9 示例(信号量实现线程同步)
cpp
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
// 定义信号量(初始值为0,让线程B先阻塞)
sem_t sem;
// 线程A:先执行,执行完释放资源
void* thread_A(void* arg) {
printf("线程A:开始执行任务...\n");
sleep(2); // 模拟耗时操作
printf("线程A:任务执行完成,释放信号量\n");
sem_post(&sem); // 释放资源,计数器+1
return NULL;
}
// 线程B:等待线程A完成后执行
void* thread_B(void* arg) {
printf("线程B:等待线程A完成...\n");
sem_wait(&sem); // 申请资源,初始计数器为0,阻塞等待
printf("线程B:被唤醒,开始执行任务\n");
return NULL;
}
int main() {
pthread_t tidA, tidB;
// 初始化信号量:线程间共享(pshared=0),初始值0
sem_init(&sem, 0, 0);
// 创建线程(先创建B,再创建A,验证同步效果)
pthread_create(&tidB, NULL, thread_B, NULL);
sleep(1); // 确保线程B先执行到sem_wait
pthread_create(&tidA, NULL, thread_A, NULL);
// 等待线程结束
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
// 销毁信号量
sem_destroy(&sem);
return 0;
}