【Linux】线程管理——互斥锁

一、问题引入------if语句也会"失效"?

写代码时,我们总以为条件判断是"可靠的"------比如在多线程抢票逻辑中,加一句 if (count > 0) 就能保证只在有余票时扣减票数。但实际运行时,票数却离奇地出现了负数:明明判断了 count>0,为啥还会执行 count-- 把票数减成负数? 先看这段看似"没问题"的多线程抢票代码:

cpp 复制代码
#include <pthread.h>
#include <iostream>
#include<unistd.h>
int count = 1000;
void *routine(void *args)
{
    const char *name = static_cast<char *>(args);
    while (1)
    {
        if (count > 0)
        {
            usleep(1000);//模拟抢票时间
            count--;
            std::cout << name << "抢到了票,剩余票数:" << count << std::endl;
        }
        else
            break;
    }
    return nullptr;
}
int main()
{
    pthread_t td1, td2, td3;
    pthread_create(&td1, nullptr, routine, (void *)"pthread -1");
    pthread_create(&td2, nullptr, routine, (void *)"pthread -2");
    pthread_create(&td3, nullptr, routine, (void *)"pthread -3");
    pthread_join(td1,nullptr);
    pthread_join(td2,nullptr);
    pthread_join(td3,nullptr);
}

代码看似逻辑完全正确,不会出现问题,但是实际运行却出现了致命问题------出现负数!

要弄懂这个问题,首先需要明确以下概念:

原子性

原子性指一个操作(或一组操作)的执行过程不可被中断,要么完全执行完毕,要么完全不执行,不存在 "执行了一半" 的中间状态;且在该操作执行期间,其他线程 / 进程无法观察到操作的中间结果,只能看到操作执行前或执行后的最终状态。

对于一个count--的代码段,看似只有一条指令,但是转为汇编语言其实分了三步:将count从内存载入CPU寄存器->CPU进行算数运算->CPU寄存器将数值写回内存:

cpp 复制代码
//伪代码
movl count, %ebx   ; 第一步:把内存中count的值载入CPU的ebx寄存器
subl $1, %ebx      ; 第二步:ebx寄存器中的值减1(计算)
movl %ebx, count   ; 第三步:把ebx寄存器的结果写回内存中的count

这样分为三步的操作很明显就不符合原子性概念,但是单独每条汇编操作都具有原子性。

有了原子性的概念,结合进(线)程切换的话题,容易想到:可能存在线程对于一条count--的指令,可能只执行了前一段或者两段汇编指令,就触发了线程切换,由于老线程还没有执行将寄存器count值写回内存的指令,导致新线程拿到的count值依然是修改前的count值,一定程度上就产生了安全问题。

问题解答

有了以上概念就可以很容易地得出问题答案------为什么if语句会"失效"

**if语句也不具有原子性------**if语句看似只有一句,但是转为汇编也分为了三步:将内存中的count载入CPU寄存器->进行逻辑运算执行判断->根据运算结果进行函数跳转

cpp 复制代码
//if部分伪代码
loop_start:
    movl count, %eax ; 步骤1:读取内存count到eax寄存器
    cmpl $0, %eax    ; 步骤2:eax与0比较,结果存状态寄存器
    jle loop_start   ; 步骤3:count≤0则跳到循环结束,>0则执行后续逻辑

; 满足条件时执行的count--等逻辑(省略)

此时就会发生和之前同样的安全问题,并且这里也是导致票数出现负数------if语句失效的关键:

假设只有两个线程------线程A和线程B,此时设count的值为1:

A线程 首先执行if语句,判断结果为真,将要执行下一步(跳转到count--)发生线程切换,B线程 进行if语句执行,此时count并没有发生自减,因此if语句依然为真,但是B线程执行完了count--(1->0)的指令并且写回了内存,此时再次发生线程切换,A线程切回并且恢复执行上下文,执行count--的指令,但是此时count在B线程执行期间已经发生自减变成了0,再次自减就变成了-1,也就出现了负数。

这样的问题无异于一个巨大的bug,为了解决这样的问题------就引入了线程互斥的概念。

线程互斥

临界资源以及临界区

上述模拟抢票的代码中,出现问题的根本原因其实就是多个线程并发访问修改同一个变量,对于一个会同时被多个线程进行访问的数据,称其为共享资源,对于涉及修改操作并且操作不具有原子性的共享资源,称其为临界资源 。而代码中涉及此类修改操作的代码块就称为临界区

cpp 复制代码
while (1)
    {
        if (count > 0)
        {
            usleep(1000);//模拟抢票时间
            count--;
            std::cout << name << "抢到了票,剩余票数:" << count << std::endl;
        }
        else
            break;
    }

以上面模拟抢票的代码部分为例:其中count就是共享资源中的临界资源 ,而if(count>0)与count--所在的代码块就是临界区。

多个线程并发访问并且非原子性修改同一个变量就会导致一系列安全问题出现,此时线程互斥的概念就提出了解决思路:

线程互斥概念

互斥 :指多个并发执行的线程 / 进程在访问同一临界资源时,保证同一时刻只有一个线程 / 进程能进入操作该资源的临界区;其他线程 / 进程必须等待当前线程 / 进程退出临界区后,才能进入,从而避免因并发访问导致的资源竞争和数据不一致问题。

比如上述模拟抢票的代码经过互斥优化后:

  • "同一时刻只有一个线程进入临界区" 抢票案例中,就是同一时刻只有一个线程能执行if(count>0)+count--,避免线程 A 判断后、线程 B 扣减,再切回线程 A 扣减导致负数的场景。
  • **"其他线程必须等待"**若线程 A 已进入临界区(持有锁),线程 B 会卡在加锁步骤,直到线程 A 释放锁,再重新读取 count 的最新值判断,而非基于旧值执行。
  • **"避免资源竞争和数据不一致"**这是互斥的最终目标 ------ 通过排他性访问,解决多线程并发修改 count 导致的负数(数据不一致)问题。

线程互斥实现------互斥锁

pthread库实现------pthread_mutex

pthread库对互斥锁进行了封装,其中pthread_mutex_t则是锁的类型定义,图中三个函数:

pthread_mutex_lock():阻塞式获取互斥锁,若锁已被其他线程持有则当前线程进入阻塞态(休眠),直到成功拿到锁后才继续执行,是保证临界区互斥访问的核心函数。

pthread_mutex_trylock():非阻塞式尝试获取互斥锁,仅尝试一次:锁空闲则成功拿到(返回 0),锁被占用则直接返回错误码(EBUSY),全程不阻塞、不休眠。

pthread_mutex_unlock():释放当前线程持有的互斥锁,同时唤醒该锁等待队列中阻塞的线程(让其进入就绪态竞争锁),需与成功加锁的 lock/trylock 配对调用。

其中对锁进行初始化分为两种方式:

静态初始化: 通过PTHREAD_MUTEX_INITIALIZER宏在编译期 完成互斥锁初始化,仅适用于全局 /static静态作用域的pthread_mutex_t变量,只能初始化默认属性的互斥锁,无需调用pthread_mutex_init,也可省略pthread_mutex_destroy

动态初始化: 通过pthread_mutex_init()函数在运行期 完成互斥锁初始化,适用于所有作用域的pthread_mutex_t变量(全局 / 静态 / 局部 / 堆),支持通过pthread_mutexattr_t自定义锁属性(如递归锁、健壮锁),有返回值可检测初始化失败,且必须配对调用pthread_mutex_destroy释放资源。

补充:

pthread_mutex_destroy ():

pthread_mutex_destroy()是 POSIX 线程库提供的互斥锁销毁函数 ,用于释放pthread_mutex_t类型锁对象所占用的系统资源(如内核等待队列、锁状态元数据等),仅能对 "已初始化且未被持有" 的互斥锁调用,调用后该锁对象将恢复为未初始化状态,不可再用于加锁 / 解锁操作(除非重新初始化)。

抢票代码加锁优化演示:

1、静态初始化

cpp 复制代码
#include <pthread.h>
#include <iostream>
#include <unistd.h>
int count = 1000;
pthread_mutex_t _lock = PTHREAD_MUTEX_INITIALIZER;
void *routine(void *args)
{
    const char *name = static_cast<char *>(args);
    while (1)
    {
        pthread_mutex_lock(&_lock);
        if (count > 0)
        {
            usleep(1000);
            count--;
            std::cout << name << "抢到了票,剩余票数:" << count << std::endl;
            pthread_mutex_unlock(&_lock);
        }
        else
        {
            pthread_mutex_unlock(&_lock);
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t td1, td2, td3;
    pthread_create(&td1, nullptr, routine, (void *)"pthread -1");
    pthread_create(&td2, nullptr, routine, (void *)"pthread -2");
    pthread_create(&td3, nullptr, routine, (void *)"pthread -3");
    pthread_join(td1, nullptr);
    pthread_join(td2, nullptr);
    pthread_join(td3, nullptr);
}

2、动态初始化

cpp 复制代码
#include <pthread.h>
#include <iostream>
#include <unistd.h>
int count = 1000;
pthread_mutex_t _lock; // 此处不进行初始化
void *routine(void *args)
{
    const char *name = static_cast<char *>(args);
    while (1)
    {
        pthread_mutex_lock(&_lock);
        if (count > 0)
        {
            usleep(1000);
            count--;
            std::cout << name << "抢到了票,剩余票数:" << count << std::endl;
            pthread_mutex_unlock(&_lock);
        }
        else
        {
            pthread_mutex_unlock(&_lock);
            break;
        }
    }
    return nullptr;
}
int main()
{
    // 核心区别
    int ret = pthread_mutex_init(&_lock, nullptr); // 第二个参数表示锁属性,nullptr表示默认属性
    if (ret != 0)                                  // 动态初始化成功返回0否则返回错误码
    {
        std::cerr << "互斥锁初始化失败,错误码:" << ret << std::endl;
        return -1;
    }
    pthread_t td1, td2, td3;
    pthread_create(&td1, nullptr, routine, (void *)"pthread -1");
    pthread_create(&td2, nullptr, routine, (void *)"pthread -2");
    pthread_create(&td3, nullptr, routine, (void *)"pthread -3");
    pthread_join(td1, nullptr);
    pthread_join(td2, nullptr);
    pthread_join(td3, nullptr);
    ret = pthread_mutex_destroy(&_lock);
    if (ret != 0)
    {
        std::cerr << "互斥锁销毁失败,错误码:" << ret << std::endl;
        return -1;
    }
}

运行截图:

加锁后很好的解决了数值问题并且由于锁的阻塞机制实测代码运行速度也变慢了不少。

除此之外,C++11也引入了面向对象的原生锁机制。

补充:互斥锁如何实现?

硬件级实现

结合互斥的原理:让线程有序的进行临界区访问,那么就可以通过关闭时钟中断来实现,但是此方法易导致系统出现安全问题导致死机。

软件级实现

软件级通过swap和exchange进行原子性交换操作实现上锁/解锁操作,这里的swap和exchange指的是汇编中的原子交换指令(xchg)。软件实现上,将锁记作标志位(0/1),通过原子交换进行标志位的变换,标记锁的状态。由于操作为原子性操作,因此不会出现非原子性操作产生的安全问题。

对于上锁和解锁的过程可以用以下伪代码演示:

cpp 复制代码
# 简化版加锁汇编逻辑
movb $1, %al          # 把1放到%al寄存器(准备设为锁定状态)
xchgb %al, (%rdi)     # 原子交换:返回旧值到%al
testb %al, %al        # 判断旧值是否为0(test是按位与,结果为0则ZF标志位置1)
jnz 1b                # 若旧值非0(锁被占),跳回重试(自旋)
cpp 复制代码
# 简化版解锁汇编逻辑
movb $0, %al          # 把0放到%al寄存器
xchgb %al, (%rdi)     # 原子交换:%al(0)和内存地址(%rdi)(锁标记位)的值交换
# 执行后,锁标记位被设为0(解锁)
相关推荐
林姜泽樾2 小时前
linux入门第五章,mkdir、touch详解
linux·运维·服务器
木子欢儿2 小时前
在 Debian 13(以及 12)上安装和配置 tightvncserver 并让普通用户使
运维·前端·debian
ol木子李lo2 小时前
Linux 命令备忘录
linux·运维·服务器·windows·编辑器·ssh·bash
SakitamaX2 小时前
Nginx安装与实验
服务器·前端·nginx
2501_918126912 小时前
stm32能刷什么程序?
linux·stm32·单片机·嵌入式硬件·学习
第七序章2 小时前
【Linux学习笔记】git三板斧
linux·运维·服务器·笔记·git·学习
礼拜天没时间.2 小时前
Node.js运维部署实战:从0到1开始搭建Node.js运行环境
linux·运维·后端·centos·node.js·sre
一只自律的鸡2 小时前
【Linux系统编程】文件IO 标准IO
linux·运维·服务器
郝学胜-神的一滴2 小时前
Python中的del语句与垃圾回收机制深度解析
服务器·开发语言·网络·python·算法