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.pswp.cn/pingmian/94634.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/94634.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一键搭建开发环境:制作bash shell脚本

完整脚本&#xff1a; 1.0 #!/bin/bash set -eecho " 开始安装 AI 开发环境&#xff08;无人交互版&#xff09; "# 检测是否以 sudo 运行 if [ "$EUID" -eq 0 ]; thenecho "⚠️ 警告&#xff1a;请不要使用 sudo 运行此脚本&#xff01;"echo …

mac m4执行nvm install 14.19.1报错,安装低版本node报错解决

原因 由于node14使用的变异工具链太旧&#xff0c;无法适配最新的macOS SDK头文件导致_studio.h报错 解决办法 方法1 更新nvm到最新版本 brew update nvmnvm install 14.19.1 --binary 方法2 启用Rosetta安装&#xff08;Intel仿真&#xff09; 1.arch -x86_64 zsh 2.nvm insta…

Codeforces Round 1043 (Div. 3) F. Rada and the Chamomile Valley

F.拉达和甘菊谷 每次测试的时间限制&#xff1a;3 秒 每次测试的内存限制512 兆字节 输入&#xff1a;标准输入 输出&#xff1a;标准输出 昨天&#xff0c;拉达发现了一个传送门&#xff0c;可以把她传送到洋甘菊谷&#xff0c;然后再传送回来。拉达的快乐无以言表&#xff0c…

STM32 入门实录:从 0 到 3 色 LED 呼吸式闪烁

一、需求 & 最终效果 硬件&#xff1a;Blue-Pill&#xff08;STM32F103C8T6&#xff09; 3 只 LED&#xff08;红 PA0、黄 PA1、绿 PA2&#xff09;现象&#xff1a;上电后红→黄→绿→黄→全灭&#xff0c;每步 100 ms&#xff0c;循环往复。 二、硬件连接LED 端连接说明阴…

Playwright与PyTest结合指南

Playwright与PyTest的结合&#xff0c;为Web自动化测试带来了强大的动力。它让你既能利用Playwright现代、跨浏览器的自动化能力&#xff0c;又能借助PyTest成熟测试框架的结构化、可扩展性来高效管理和组织测试用例。我会带你了解如何将这两者结合使用。 为了让你快速上手&am…

plantsimulation知识点 一条轨道上多台RGV如何引用

最近做项目有如下需求&#xff1a;轨道1上初始化生成三台RGV&#xff0c;然后通过另一条轨道2上的传感器代码控制轨道1上的三台RGV&#xff0c;之前如果另一条轨道只有一台RGV&#xff0c;我是通过轨道2.cont来引用这台RGV的。但是现在轨道上有了多台RGV&#xff0c;此代码就不…

【Canvas与盾牌】“靡不有初,鲜克有终”黄竖条盾牌

【成图】【代码】<!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>黄竖条盾牌 Draft1</title><style type"text/css"&…

使用linux+javascript+html+mysql+nodejs+npm+express等构建信息资料采集系统

一、适用场景 1、人才信息库、档案管理&#xff0c;构建企业或单位内部人才库。 2、公务员/事业单位招聘&#xff0c;网上报名填写资料、上传证书等。 3、科研项目申报&#xff0c;课题负责人信息、成果附件、审查材料上传。 4、志愿者招募&#xff1a;在线填写报名信息&#…

低空经济产业白皮书:音视频链路在智能飞行体系中的核心地位

引言 低空经济正在成为继数字经济、新能源产业之后的又一战略制高点。它不仅意味着无人机物流、空中通勤、应急救援、文旅体验等新业态的兴起&#xff0c;更代表着 城市治理、智能制造、公共服务全面进入空域数字化时代。从政策引导到产业投资&#xff0c;从技术突破到应用创新…

【LeetCode 热题 100】32. 最长有效括号——(解法二)动态规划

Problem: 32. 最长有效括号 文章目录整体思路完整代码时空复杂度时间复杂度&#xff1a;O(N)空间复杂度&#xff1a;O(N)整体思路 这段代码同样旨在解决 “最长有效括号” 问题&#xff0c;但它采用的是一种 动态规划 (Dynamic Programming) 的方法。这种方法通过构建一个DP表…

使用Docker部署ZLMediaKit流媒体服务器实现gb/t28181协议的设备

最近在研究一个摄像头&#xff0c;通信协议是 gb/t28181。对于这个协议也是第一次接触&#xff0c;通过查阅多方资料&#xff0c;找到了两个开源的源码&#xff0c;来实现 视频播放、摄像头直播。以前也没有深入的了解过关于视频播放的这方面的技术&#xff0c;偶尔网站播放视频…

硬件三人行--运算基础篇

第3讲 负反馈放大电路

【LINUX网络】TCP原理

目录 本文介绍 1. 什么是TCP&#xff1f; 2. TCP结构 为什么需要协议栈&#xff1a;两台主机通信的复杂性解决方案 3. 确认应答机制 进一步理解什么是确认和请求以及序号 进一步理解什么是序号和确认序号 并发发送带来的问题以及解决方案&#xff08;序号&#xff09; …

Java -- 文件基础知识--Java IO流原理--FileReader

目录 1. 常用文件操作 2. Java IO流原理 2.1 流的分类 3. FileReader和FileWriter介绍 FileReader相关方法&#xff1a; FileWriter常用方法&#xff1a; 文件是保存数据的地方&#xff0c;比如大家经常使用的word文档&#xff0c;txt文件&#xff0c;excel文件...都是文…

向量方法证明正余弦定理的数学理论体系

向量方法证明正余弦定理的数学理论体系 摘要&#xff1a; 向量理论为几何定理的证明提供了强有力的代数化工具。本文基于向量空间的基本概念与运算性质&#xff0c;严格推导平面几何中的正弦定理与余弦定理。通过建立系统的向量表示框架&#xff0c;将几何关系转化为向量运算&a…

【笔记ing】大模型算法架构

前言 随着人工智能技术的飞速发展,大模型算法及其架构已成为推动科技前沿的重要力量。它们不仅能够处理海量的数据,还具备强大的表征学习能力,能够应对日益复杂的场景需求。本章节将介绍大模型算法及其架构,带您了解其背后的原理、技术创新以及在实际应用中的广阔前景。 …

ConcurrentHashMap的原理

1.底层数据结构JDK1.7底层采用分段的数组链表实现JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组链表/红黑二叉树2.加锁的方式JDK1.7采用Segment分段锁,底层使用的是ReentrantLockJDK1.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁…

【论文阅读】健全个体无辅助运动期间可穿戴传感器双侧下肢神经机械信号的基准数据集

Benchmark Datasets for Bilateral Lower-Limb Neuromechanical Signals from Wearable Sensors during Unassisted Locomotion in Able-Bodied Individuals 原文&#xff1a;DOI&#xff1a; 10.3389/frobt.2018.00014 2018年 翻译&#xff1a;靠岸学术 目录 1引言 2仪器设…

反向海淘系统搭建:从架构设计到合规运营的全方位指南

一、系统架构设计1.1 分层架构设计反向海淘系统通常采用四层架构设计&#xff1a;‌接入层‌&#xff1a;负责与淘宝开放平台、1688海外接口通信&#xff0c;处理接口认证、请求转发与响应解析。‌业务层‌&#xff1a;包含商品检索、订单管理、支付处理、物流追踪等核心模块。…

20.22 QLoRA微调实战:中文语音识别数据准备全流程解密

QLoRA微调实战:中文语音识别数据准备全流程解密 实战项目:QLoRA 微调数据准备详解 本环节我们将以中文语音识别任务为场景,详细拆解 QLoRA 微调前的数据准备流程。以下流程图展示了完整的数据处理路径: #mermaid-svg-A3ZpWn1ysZUg6jg4 {font-family:"trebuchet ms&q…