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

linux-线程互斥

现象介绍

线程的互斥是多线程编程中,为避免多个线程同时操作共享资源而导致数据不一致或错误,通过特定机制限制同一时间只有一个线程访问共享资源的技术。

核心原因

多个线程并发执行时,若同时读写共享资源(如全局变量、数据库记录等),可能引发“竞态条件”。例如,两个线程同时读取一个变量并修改,可能导致最终结果错误(如银行转账时,两笔转账同时操作余额,可能导致金额计算错误)。

两个线程同时调用 全局或共享数据时,由于修改不是原子操作(拆分为“读-改-写”三步),会出现线程交错执行的情况,导致最终结果不符合预期

解决的思路

保证执行流得原子性

解决方法

使用锁

1.加锁的本质是用时间来换取安全,其表现为线程对于临界区代码的串行执行。加锁的原则为尽量要保证临界区代码越少越好。

2.我们无需担心锁会像执行流一样出现互斥现象,因为所本身就是共享资源,所以申请和释放所本身就是被设计成为原子性操作的
深入理解Linux线程锁:从竞态条件到互斥保护

先看一段简单的多线程代码:两个线程同时对全局变量 count 执行10000次自增操作。

#include <stdio.h>
#include <pthread.h>int count = 0; // 共享资源void *increment(void *arg) {for (int i = 0; i < 10000; i++) {count++; // 看似简单的自增,实际是"读-改-写"三步操作}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\n", count); // 预期20000,实际往往小于它return 0;
}




运行后会发现,结果几乎不可能是20000。这是因为 count++ 并非原子操作,当两个线程同时读取 count 的旧值、各自加1再写回时,就会出现“覆盖”现象——比如线程A和B同时读到 count=100 ,各自加1后写回,最终 count 只增加了1,而非预期的2。这种因多线程无序访问共享资源导致的异常,被称为竞态条件(Race Condition)。

解决竞态条件的核心思路很简单:让共享资源的访问变成“原子操作”,即同一时间只允许一个线程操作。而线程锁,就是实现这一目标的关键工具。

二、Linux中的线程锁

Linux系统提供了多种线程锁机制,适用于不同场景。

互斥锁(Mutex)

互斥锁(Mutual Exclusion)是最常用的线程锁,核心功能是保证同一时间只有一个线程能持有锁,从而独占对共享资源的访问权。它的使用流程像“开门-办事-关门”:

- 线程访问共享资源前,尝试“获取锁”( pthread_mutex_lock );
- 若锁未被占用,线程获取锁并进入临界区(操作共享资源);
- 若锁已被占用,线程会阻塞等待,直到锁被释放;
- 操作完成后,线程“释放锁”( pthread_mutex_unlock ),让其他线程有机会获取锁。

代码示例(修复上述反例):

#include <stdio.h>
#include <pthread.h>int count = 0;
pthread_mutex_t mutex; // 定义互斥锁void *increment(void *arg) {for (int i = 0; i < 10000; i++) {pthread_mutex_lock(&mutex); // 加锁count++; pthread_mutex_unlock(&mutex); // 解锁}return NULL;
}int main() {pthread_t t1, t2;pthread_mutex_init(&mutex, NULL); // 初始化锁pthread_create(&t1, NULL, increment, NULL);pthread_create(&t2, NULL, increment, NULL);pthread_join(t1, NULL);pthread_join(t2, NULL);printf("最终结果:%d\n", count); // 始终为20000pthread_mutex_destroy(&mutex); // 销毁锁return 0;
}

注意点:

- 必须在使用前通过 pthread_mutex_init 初始化,使用后通过 pthread_mutex_destroy 销毁,避免资源泄漏;

PTHREAD_MUTEX_INITIALIZER  是 POSIX 线程库定义的一个宏,用于对 pthread_mutex_t  类型的互斥锁进行静态初始化。它的作用相当于在编译阶段就完成了锁的初始化工作,无需调用 pthread_mutex_init  函数。


- 加锁与解锁必须成对出现,漏解锁会导致其他线程永久阻塞(死锁风险)。



三、使用线程锁的“避坑指南”

线程锁虽好,但使用不当会引入新问题,这几个“坑”一定要避开:

1. 死锁:最常见的“线程陷阱”

死锁是指两个或多个线程互相等待对方释放锁,导致所有线程永久阻塞的状态。比如:

- 线程A持有锁1,等待锁2;
- 线程B持有锁2,等待锁1;
- 两者无限等待,程序卡死。

避免死锁的原则:

- 按固定顺序获取锁(如所有线程都先获取锁1,再获取锁2);
- 避免在持有锁时调用外部函数(可能间接获取其他锁);
- 使用 pthread_mutex_trylock (非阻塞获取锁),超时后主动释放已持有的锁。

2. 锁粒度:不是越细越好

锁的“粒度”指临界区的大小:

- 粗粒度锁:一个锁保护大量共享资源,实现简单但并发低;
- 细粒度锁:多个锁分别保护不同资源,并发高但复杂度高。

合理的做法是:在保证正确性的前提下,尽量缩小临界区范围(比如只锁修改共享资源的代码,而非整个函数),但不必过度拆分导致代码难以维护。

3. 不要重复加锁

同一线程对同一把锁重复加锁(未解锁时再次调用 pthread_mutex_lock ),会导致死锁(自己等自己)。若确实需要重入,可使用“递归互斥锁( PTHREAD_MUTEX_RECURSIVE )”,但需谨慎——递归锁可能隐藏代码逻辑问题。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

原理

从汇编到线程互斥本质

我们从汇编指令、线程上下文、硬件交互三个维度,拆解锁(以 pthread_mutex_t 为例)的工作原理,让“互斥”不再抽象:



一、核心逻辑:用原子指令实现“互斥权交换”

锁的本质是用一条“原子汇编指令”(如 xchgb ),实现线程对“锁资源”的独占性交换,核心流程分两步:

1. 加锁(lock):用 xchgb 原子交换,抢占锁权

lock:movb $0, %al      ; 把0存入al寄存器(代表“我要抢占锁”)xchgb %al, mutex  ; 关键!原子交换:内存中mutex的值 ↔ al寄存器的值if (%al > 0) {    ; 判断交换前,mutex里存的是啥return 0;     ; 如果>0,说明抢到锁(之前没人持有)} else {挂起等待;     ; 如果=0,说明锁被占用,当前线程休眠goto lock;    ; 被唤醒后,重新尝试抢占}

关键细节: xchgb 的原子性

- 交换本质:把内存( mutex )和CPU寄存器( %al )的数据原子交换,中间不会被其他线程打断。
- 锁状态约定: mutex=1 表示“锁可用”, mutex=0 表示“锁被占用”(约定可自定义,核心是原子交换)。


2. 解锁(unlock):释放锁权,唤醒等待线程

unlock:movb $1, mutex    ; 把mutex设为1(释放锁,标记“可用”)唤醒等待mutex的线程; 通知操作系统:之前休眠的线程可以竞争锁了return 0;


关键细节:“唤醒”的内核参与

- 线程挂起/唤醒由操作系统内核管理:解锁时,内核从“等待队列”中挑一个线程,把它从“阻塞态”改回“就绪态”,让CPU重新调度。



二、硬件与内存的交互:线程上下文的“交换本质”

**“锁是线程上下文交换的载体”**

1. 内存是“共享黑板”:
所有线程都能访问同一块内存(如 int mutex ), xchgb 让线程把“自己寄存器里的0”和“黑板上的mutex值”交换,本质是用硬件原子指令,实现“我要占锁”的宣告。
2. 寄存器是“线程私有空间”:
每个线程有独立的寄存器(如 %al ), xchgb 把内存的公共状态,交换到线程的私有上下文里,完成“锁权归属”的判定。

三、多线程竞争的完整流程(结合线程1、线程2)

假设 mutex 初始值为 1 (锁可用),看两个线程如何竞争:

线程1执行 pthread_mutex_lock :

1.  movb $0, %al  →  %al=0 
2.  xchgb %al, mutex  → 内存 mutex=0 , %al=1 (原子交换)
3. 判断 %al>0 (成立)→ 线程1成功拿到锁,进入临界区。

线程2执行 pthread_mutex_lock :

1.  movb $0, %al  →  %al=0 
2.  xchgb %al, mutex  → 内存 mutex=0 (线程1已占), %al=0 (交换后的值)
3. 判断 %al>0 (不成立)→ 线程2被挂起等待,进入内核的“阻塞队列”,CPU切换去执行其他任务。

线程1执行 pthread_mutex_unlock :

1.  movb $1, mutex  → 内存 mutex=1 (释放锁)
2. 内核从“阻塞队列”唤醒线程2 → 线程2进入“就绪态”,等待CPU调度。

线程2被唤醒后:

重新执行 lock 逻辑:

-  movb $0, %al  →  %al=0 
-  xchgb %al, mutex  → 内存 mutex=0 , %al=1 (原子交换)
- 判断 %al>0 (成立)→ 线程2拿到锁,进入临界区。 

四、总结:锁的本质是“原子交换 + 上下文调度”

1. 原子指令保证“抢锁公平”:
xchgb 让“抢锁”操作不会被打断,避免多个线程同时拿到锁。
2. 内核调度实现“阻塞/唤醒”:
没抢到锁的线程会被内核挂起,不浪费CPU;解锁时再由内核唤醒,实现“有序竞争”。
3. 锁是线程间的“信号灯”:
通过内存中的 mutex 值,线程间实现了“我在用,你等着”的通信,本质是用硬件原子性 + 内核调度,解决多线程共享资源的竞争问题。

这就是锁的底层逻辑—— 看似简单的 pthread_mutex_lock/unlock ,背后是硬件原子指令和操作系统内核的深度协同!

希望这篇文章能帮你理清Linux线程锁的核心逻辑,让你的多线程程序既高效又稳定!

http://www.lryc.cn/news/589055.html

相关文章:

  • 硬件设计学习DAY1——电源的分类
  • HAProxy 简介及配置文件详解
  • nlp论文:分本分类:《Bag of Tricks for Efficient Text Classification》
  • 渭河SQL题库-- 来自渭河数据分析
  • 5.数据归一化
  • Python爬虫实战:研究Mistune库相关技术
  • UE5多人MOBA+GAS 23、制作一个地面轰炸的技能
  • Typecho插件开发:实现文章字数统计与阅读时长计算功能
  • Docker镜像导入、导出操作指南
  • 大型语言模型(LLM)的技术面试题
  • 如何通过 WebSocket 接口订阅实时外汇行情数据(PHP 示例)
  • 深入探讨Hadoop YARN Federation:架构设计与实践应用
  • CentOS 8-BClinux8.2更换为阿里云镜像源:保姆级教程
  • Linux、Ubuntu和CentOS的关系与区别
  • RNN、GRU 与 LSTM 计算成本深入对比
  • 贪心贪心的反悔
  • 大语言模型零样本情感分析实战:无需机器学习训练,96%准确率实现指南
  • 003大模型基础知识
  • QT——文件选择对话框 QFileDialog
  • Perfectly Clear WorkBench V4.6.1.2731图像后期处理调色工具安装部署
  • 3.2数据库-关系代数-函数依赖-范式
  • 深度强化学习 | 图文详细推导深度确定性策略梯度DDPG算法
  • linux网络编程之单reactor模型(二)
  • Web攻防-PHP反序列化字符逃逸增多减少成员变量属性解析不敏感Wakeup绕过
  • 第二章 数据的表示和运算
  • 【每天一个知识点】多模态信息(Multimodal Information)
  • 为何说分布式 AI 推理已成为下一代计算方式
  • AI-Compass LLM训练框架生态:整合ms-swift、Unsloth、Megatron-LM等核心框架,涵盖全参数/PEFT训练与分布式优化
  • 分布式通信框架 - JGroups
  • 第二阶段-第二章—8天Python从入门到精通【itheima】-129节(MySQL的安装)