linux-线程互斥

发布于:2025-07-16 ⋅ 阅读:(12) ⋅ 点赞:(0)

现象介绍

线程的互斥是多线程编程中,为避免多个线程同时操作共享资源而导致数据不一致或错误,通过特定机制限制同一时间只有一个线程访问共享资源的技术。
 
核心原因
 
多个线程并发执行时,若同时读写共享资源(如全局变量、数据库记录等),可能引发“竞态条件”。例如,两个线程同时读取一个变量并修改,可能导致最终结果错误(如银行转账时,两笔转账同时操作余额,可能导致金额计算错误)。

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

解决的思路

保证执行流得原子性

解决方法

使用锁

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); // 始终为20000
    pthread_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线程锁的核心逻辑,让你的多线程程序既高效又稳定!


网站公告

今日签到

点亮在社区的每一天
去签到