当前位置: 首页 > news >正文

Linux系统 -- 多线程的控制(互斥与同步)

在多线程编程中,多个线程可能同时访问临界资源(如共享变量、文件、硬件设备等),若缺乏控制会导致数据混乱。互斥同步是解决该问题的核心机制,其中互斥锁保证临界资源的排他访问,信号量实现线程间的有序协作。以下是核心知识点整理:

一、互斥(Mutex):保证临界资源的排他访问

1. 核心概念

  • 临界资源:多线程共同访问的公共资源(如全局变量、共享内存、文件句柄)。
  • 互斥:对临界资源的排他性访问—— 同一时间仅允许一个线程访问,其他线程需阻塞等待,直到当前线程释放资源。
  • 互斥锁:实现互斥机制的核心工具,本质是 “锁”,通过 “加锁 - 访问 - 解锁” 的流程控制临界资源的访问权限。

2. 互斥锁的使用流程

互斥锁的使用需遵循 “定义→初始化→加锁→解锁→销毁” 的五步流程,所有操作均需包含 <pthread.h> 头文件。

(1)定义互斥锁

声明一个 pthread_mutex_t 类型的变量,代表一个互斥锁:

pthread_mutex_t mutex;  // 定义互斥锁
(2)初始化互斥锁

通过 pthread_mutex_init 函数初始化已定义的互斥锁,设置锁的属性。

函数原型int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
功能初始化指定的互斥锁,分配锁的资源
参数mutex:指向待初始化的互斥锁变量的指针
attr:锁的属性(通常设为 NULL,表示使用默认属性)
返回值- 成功:返回 0
- 失败:返回非 0(错误码,可通过 perror 打印错误信息)
// 初始化互斥锁,使用默认属性
if (pthread_mutex_init(&mutex, NULL) != 0) {perror("pthread_mutex_init failed");exit(1);
}
(3)加锁(P 操作的简化)

通过 pthread_mutex_lock 函数为临界区 “上锁”,确保同一时间仅一个线程进入临界区。

函数原型int pthread_mutex_lock(pthread_mutex_t *mutex);
功能尝试获取互斥锁:
- 若锁未被占用,立即获取并继续执行
- 若锁已被占用,当前线程阻塞等待
参数mutex:指向待加锁的互斥锁变量的指针
返回值- 成功:返回 0
- 失败:返回非 0

核心注意事项

  • 加锁后到解锁前的代码段称为 临界区,该区域的操作具有 原子性(CPU 多条指令不可分割,必须一次性执行完毕)。
  • 其他线程在临界区未解锁时,调用 pthread_mutex_lock 会阻塞,直到锁被释放。
  • 临界区需 尽可能小(仅包含访问临界资源的代码),避免因耗时操作导致其他线程长期阻塞。
(4)解锁(V 操作的简化)

通过 pthread_mutex_unlock 函数释放互斥锁,允许其他阻塞的线程获取锁并进入临界区。

函数原型int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能释放已获取的互斥锁,唤醒等待该锁的线程(若有)
参数mutex:指向待解锁的互斥锁变量的指针
返回值- 成功:返回 0
- 失败:返回非 0(如未加锁却解锁)

注意:解锁操作必须与加锁操作成对出现,且需在同一线程中执行(不可跨线程解锁)。

(5)销毁互斥锁

互斥锁不再使用时,需通过 pthread_mutex_destroy 函数销毁,释放锁占用的系统资源。

函数原型int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能销毁互斥锁,释放其占用的内存和系统资源
参数mutex:指向待销毁的互斥锁变量的指针
返回值- 成功:返回 0
- 失败:返回非 0(如锁未初始化或仍被占用)
// 销毁互斥锁
if (pthread_mutex_destroy(&mutex) != 0) {perror("pthread_mutex_destroy failed");exit(1);
}

3. 互斥锁的关键特性

  • 排他性:同一时间仅一个线程持有锁,确保临界资源的独占访问
  • 阻塞性pthread_mutex_lock 会阻塞等待,直到锁可用(避免 CPU 空转)。
  • 单一对应:一个互斥锁通常对应一个临界资源(若多个资源无关联,需定义多个锁)。

4. 互斥锁的应用举例

1、计数器保护

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>pthread_mutex_t mutex;  // 定义互斥锁
int count = 0;  // 共享资源(临界资源)// 线程函数:对共享计数器进行5000次递增
void* th(void* arg) {int i = 5000;while (i--) {pthread_mutex_lock(&mutex);  // 加锁(进入临界区)int tmp = count;  // 读取当前值printf("count:%d\n", tmp + 1);  // 打印新值count = tmp + 1;  // 更新计数器pthread_mutex_unlock(&mutex);  // 解锁(退出临界区)}return NULL;
}int main(int argc, char** argv) {pthread_t tid1, tid2;pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁// 创建两个线程pthread_create(&tid1, NULL, th, NULL);pthread_create(&tid2, NULL, th, NULL);// 等待线程结束pthread_join(tid1, NULL);pthread_join(tid2, NULL);pthread_mutex_destroy(&mutex);  // 销毁互斥锁return 0;
}

注:输出严格按1-10000顺序递增,无重复或跳号,证明互斥锁有效保护了共享资源

2、资源池管理

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>int WIN = 3;  // 可用资源数量(临界资源)
pthread_mutex_t mutex;  // 互斥锁// 线程函数:模拟资源获取与释放
void* th(void* arg) {while (1) {pthread_mutex_lock(&mutex);  // 加锁if (WIN > 0) {  // 检查资源可用性WIN--;  // 占用资源pthread_mutex_unlock(&mutex);  // 解锁(允许其他线程检查)printf("get win...\n");  // 获取资源成功sleep(rand() % 5 + 1);  // 模拟资源使用时间(1-5秒)printf("release win...\n");  // 释放资源pthread_mutex_lock(&mutex);  // 重新加锁WIN++;  // 释放资源pthread_mutex_unlock(&mutex);  // 解锁break;} else {pthread_mutex_unlock(&mutex);  // 无资源时释放锁}}return NULL;
}int main(int argc, char** argv) {pthread_t tid[10] = {0};srand(time(NULL));  // 初始化随机种子pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁// 创建10个线程for (int i = 0; i < 10; i++) {pthread_create(&tid[i], NULL, th, NULL);}// 等待所有线程结束for (int i = 0; i < 10; i++) {pthread_join(tid[i], NULL);}pthread_mutex_destroy(&mutex);  // 销毁互斥锁return 0;
}

注:任意时刻最多3个线程同时持有资源,资源获取/释放顺序可能不同,但不会出现超过3个"get win"连续出现

二、同步(Semaphore):保证临界资源的有序访问

1. 核心概念

  • 同步:在互斥的基础上,进一步要求线程按 特定顺序 访问临界资源(如 “生产者先生产,消费者后消费”)。
  • 信号量:实现同步机制的工具,本质是一个 “计数器”,通过 PV 操作(申请 / 释放资源)控制线程的执行顺序。
  • 分类
    • 无名信号量:用于 线程间同步(共享内存空间中的信号量,仅同一进程内的线程可见)。
    • 有名信号量:用于 进程间同步(通过文件系统中的名称标识,不同进程可访问)。

2. 信号量的使用流程(以无名信号量为例)

信号量的使用需遵循 “定义→初始化→PV 操作→销毁” 的流程,所有操作均需包含 <semaphore.h> 头文件。

(1)定义信号量

声明一个 sem_t 类型的变量,代表一个信号量:

sem_t sem;  // 定义无名信号量
(2)初始化信号量

通过 sem_init 函数初始化已定义的信号量,设置信号量的类型和初始值。

函数原型int sem_init(sem_t *sem, int pshared, unsigned int value);
功能初始化信号量,设置其共享范围和初始计数
参数sem:指向待初始化的信号量变量的指针
pshared:共享范围(0 表示线程间使用,非0 表示进程间使用)
value:信号量初始值(二值信号量通常设为 0 或 1,计数信号量可设为大于 1 的值)
返回值- 成功:返回 0
- 失败:返回 -1(错误码,可通过 perror 打印)

关键说明

  • 二值信号量:初始值为 0 或 1,对应 “资源不可用” 或 “资源可用”,常用于同步。
  • 计数信号量:初始值大于 1,表示可同时允许 value 个线程访问资源。
// 初始化线程间使用的二值信号量,初始值为 0(资源不可用)
if (sem_init(&sem, 0, 0) != 0) {perror("sem_init failed");exit(1);
}
(3)PV 操作(核心)

信号量的核心是 PV 操作,通过 “申请资源(P)” 和 “释放资源(V)” 控制线程执行顺序。

① P 操作(申请资源):sem_wait

尝试获取信号量对应的资源,若资源不足则阻塞等待。

函数原型int sem_wait(sem_t *sem);
功能申请资源:
- 若信号量计数 value > 0,则 value -= 1,线程继续执行
- 若信号量计数 value == 0,线程阻塞等待,直到其他线程执行 V 操作
参数sem:指向待操作的信号量变量的指针
返回值- 成功:返回 0
- 失败:返回 -1
② V 操作(释放资源):sem_post

释放信号量对应的资源,唤醒阻塞等待的线程(若有)。

函数原型int sem_post(sem_t *sem);
功能释放资源:
- 信号量计数 value += 1(无论当前值是否为 0
- 若有线程因该信号量阻塞,唤醒其中一个线程
参数sem:指向待操作的信号量变量的指针
返回值- 成功:返回 0
- 失败:返回 -1
(4)销毁信号量

信号量不再使用时,通过 sem_destroy 函数销毁,释放其占用的系统资源。

函数原型int sem_destroy(sem_t *sem);
功能销毁信号量,释放其占用的内存和系统资源
参数sem:指向待销毁的信号量变量的指针
返回值- 成功:返回 0
- 失败:返回 -1(如信号量未初始化或仍有线程等待)

3. 信号量的关键特性

  • 有序性:通过 PV 操作的先后顺序,强制线程按预期流程执行(如 “先生产后消费”)。
  • 灵活性:支持二值信号量(同步)和计数信号量(资源限流),适用场景更广。
  • 跨线程 / 进程:无名信号量支持线程间同步,有名信号量支持进程间同步。

三、互斥与同步的对比

维度互斥(Mutex)同步(Semaphore)
核心目标保证临界资源的排他访问(防数据混乱)保证线程按特定顺序访问资源(防执行顺序错误)
顺序要求无顺序要求(只要排他即可)有明确顺序要求(如 “先 A 后 B”)
操作主体加锁和解锁必须在同一线程中执行PV 操作通常在不同线程中执行(如生产者 V、消费者 P)
资源计数仅支持 “0/1” 二值(锁占用 / 未占用)支持二值(0/1)或计数(>1)
典型场景多线程修改同一全局变量、共享文件写操作生产者 - 消费者模型、任务队列的先后执行
关系同步是 “有顺序要求的互斥”,互斥是同步的基础基于互斥,进一步解决顺序问题

四、死锁(Deadlock):成因、必要条件与解决方案

1. 核心概念

  • 死锁:多个线程因互相等待对方持有的资源,导致所有线程永久阻塞(代码停滞,无法继续执行)。

2. 死锁的成因

  1. 系统资源不足:多个线程争夺有限的资源(如互斥锁、信号量)。
  2. 进程推进顺序不当:线程按错误的顺序申请 / 释放资源(如线程 A 先锁资源 1,线程 B 先锁资源 2,再互相申请对方的资源)。
  3. 资源分配不当:资源分配策略未考虑线程的依赖关系(如一次性分配所有资源,或不允许资源抢占)。

3. 死锁的四个必要条件(缺一不可)

只有同时满足以下四个条件,才会发生死锁:

  1. 互斥条件:资源只能被一个线程占用(如互斥锁的排他性)。
  2. 请求与保持条件:线程持有已获得的资源,同时申请新的资源;若新资源不可得,不释放已持有的资源。
  3. 不剥夺条件:线程已获得的资源,在未主动释放前,不能被其他线程强行剥夺。
  4. 循环等待条件:多个线程形成 “循环依赖”(如线程 A 等线程 B 的资源,线程 B 等线程 A 的资源)。

4. 死锁的解决方案

(1)避免死锁:破坏必要条件
  • 破坏 “请求与保持”:线程申请资源前,先释放已持有的所有资源(如 “一次性申请所有资源,否则不申请”)。
  • 破坏 “循环等待”:按固定顺序申请资源(如所有线程均先申请资源 1,再申请资源 2)。
  • 破坏 “不剥夺”:允许线程在超时后释放已持有的资源(如使用非阻塞锁)。
(2)解决死锁:使用非阻塞锁 / 信号量

当线程申请资源时,若资源不可用,不阻塞等待,而是立即返回错误,避免死锁。常用函数如下:

① 非阻塞互斥锁:pthread_mutex_trylock
函数原型int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能尝试获取互斥锁:
- 若锁未被占用,立即加锁并返回 0
- 若锁已被占用,不阻塞,直接返回非 0(错误码)
参数mutex:指向待加锁的互斥锁变量的指针
返回值- 成功:返回 0
- 失败:返回非 0(如锁已被占用)
// 尝试加锁,若失败则重试或处理
if (pthread_mutex_trylock(&mutex) != 0) {printf("锁已被占用,稍后重试\n");sleep(1);  // 延迟后重试// 或直接放弃,避免阻塞
}
② 非阻塞信号量:sem_trywait
函数原型int sem_trywait(sem_t *sem);
功能尝试申请信号量资源:
- 若资源可用(value > 0),value -= 1 并返回 0
- 若资源不可用(value == 0),不阻塞,直接返回 -1
参数sem:指向待操作的信号量变量的指针
返回值- 成功:返回 0
- 失败:返回 -1

五、总结

  1. 互斥锁是 “排他工具”,解决 “多线程抢资源” 的问题,确保临界资源的独占访问。
  2. 信号量是 “有序工具”,解决 “多线程按顺序执行” 的问题,在互斥基础上实现同步。
  3. 死锁是多线程编程的常见陷阱,需通过 “破坏必要条件” 或 “使用非阻塞操作” 避免。
  4. 实际开发中,需根据场景选择工具:仅需排他用互斥锁,需顺序控制用信号量;同时注意临界区最小化、资源申请顺序统一,减少死锁风险。
http://www.dtcms.com/a/355554.html

相关文章:

  • 数学思维好题(冯哈伯公式)-平方根下取整求和
  • 个人博客运行3个月记录
  • 了解ADS中信号和电源完整性的S参数因果关系
  • Typora 教程:从零开始掌握 Markdown 高效写作
  • MySQL事务ACID特性
  • JavaScript中的BOM,DOM和事件
  • 英语单词:ad-hoc
  • BugKu Web渗透之成绩查询
  • 白杨SEO:网站优化怎么做?应用举例和适合哪些企业做?参考
  • 速成Javascript(二)
  • 新书速览|SQL Server运维之道
  • 【第三方网站运行环境测试:服务器配置(如Nginx/Apache)的WEB安全测试重点】
  • 激活函数篇(3):Softmax
  • maven scope 详解
  • 通信原理实验之线性均衡器-迫零算法
  • dht11传感器总结
  • [灵动微电子 MM32BIN560CN MM32SPIN0280]读懂电机MCU之串口DMA
  • 【C++游记】子承父业——乃继承也
  • 91美剧网官网入口 - 最新美剧资源在线观看网站
  • 保姆级教程 | 在Ubuntu上部署Claude Code Plan Mode全过程
  • 【论文阅读】MotionXpert:基于肌电信号的优化下肢运动检测分类
  • Spring事务管理机制深度解析:从JDBC基础到Spring高级实现
  • [灵动微电子MM32SPIN0280]从灵动微电子看电机专用MCU
  • Deeplizard 深度学习课程(五)—— 模型训练
  • 数据结构01:顺序表
  • react Antd Table 多选大数据量 UI渲染很慢的解决方案
  • 每日五个pyecharts可视化图表日历图和箱线图:从入门到精通
  • ChatGPT登录,拒绝访问,错误1020解决办法
  • THM Whats Your Name WP
  • QT .pro文件的常见用法