从汇编角度解释线程间互斥-mutex互斥锁与lock_guard的使用

多线程并发的竞态问题

我们创建三个线程同时进行购票,代码如下

cpp 复制代码
#include<iostream>
#include<thread>
#include<list>
using namespace std;
//总票数
int ticketCount=100;
//售票线程
void sellTicket(int idx)
{
    while(ticketCount>0)
    {
        cout<<ticketCount<<endl;
        ticketCount--;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms
    }
}

int main()
{
    list<std::thread> tlist;//存储线程
    for(int i=1;i<=3;i++)//创建三个线程
    {
        tlist.push_back(std::thread(sellTicket,i));
    }

    for(auto& tl:tlist)
    {
        tl.join();//让主线程等待子线程执行结束
    }

    return 0;
}

我们再看这段代码的汇编过程

cpp 复制代码
ticketCount--;

汇编代码如下:

cpp 复制代码
mov eax,ticketCount
sub eax,1
mov ticketCount,eax

上述汇编过程的解读为:

  • 将ticketCount的值从内存放到寄存器eax
  • 通过寄存器完成减法操作
  • 将运算结果再从eax寄存器中放到内存中

可以看到,三个线程在执行代码时,每个线程在执行到ticketCount--时,在底层都会执行上述三行汇编代码,这种竞态必然会导致最终结果的错误。

如:

  • 假如现在ticketCount的值为100
  • 线程一把ticketCount的值从内存放到寄存器并完成了减法操作,则此时ticketCount的值为99,但并未将计算后的结果放到内存,也就是说此时内存中ticketCount的值仍旧为100
  • 线程二开始执行代码,那么线程二从内存取出ticketCount的值放到eax寄存器时必然为100,因此线程二在进行计算后的结果也是99
  • 之后线程一又开始继续执行代码,将他的计算结果99写回内存,则此时输出结果为99
  • 切换到线程二继续执行代码,然而线程二的结果也是99
    可以看到,本来两个线程在执行减法操作后,ticketCount的结果应该为98,但是现在的结果却都是99。
    出现上述结果的原因就在于ticketCount--代码执行的汇编过程不是一次性完成的

mutex互斥锁

这就是互斥锁出现的作用------保证ticketCount--代码的汇编过程一次性执行

  • std::mutex 是 C++11 引入的互斥量(Mutex)类,用于在多线程环境中实现互斥访问共享资源。

  • 通过 std::mutex,可以确保在同一时间只有一个线程可以访问被保护的临界区,从而避免多个线程同时对共享数据进行修改而导致的数据竞争问题。

  • std::mutex 提供了 lock() 和 unlock() 方法,分别用于锁定和解锁互斥量。需要注意的是,在编写多线程程序时,必须确保每次 lock() 操作都会有对应的 unlock() 操作,以避免死锁等问题。

修改后的代码如下:

cpp 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
using namespace std;
//总票数
int ticketCount=100;
std::mutex mtx;
//售票线程
void sellTicket(int idx)
{
    while(ticketCount>0)
    {
        mtx.lock();//加锁
        cout<<ticketCount<<endl;
        ticketCount--;//解锁
        mtx.unlock();

        std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms
    }
}

int main()
{
    list<std::thread> tlist;//存储线程
    for(int i=1;i<=3;i++)//创建三个线程
    {
        tlist.push_back(std::thread(sellTicket,i));
    }

    for(auto& tl:tlist)
    {
        tl.join();//让主线程等待子线程执行结束
    }

    return 0;
}

但是上述代码仍旧有一些问题,考虑以下情况

  • 假如ticketCount的值为1
  • 由于ticketCount大于0,因此线程一进入while循环并获取锁,但并未执行--操作,因此此时ticketCount的仍旧为1
  • 假如此时线程二刚好被切换,那么由于此时ticketCount的值还没有变化,仍旧为1大于0,因此线程二也进入while循环,但是线程一并未释放锁,因此线程将被卡住
  • 之后线程一继续执行,执行减法操作,ticketCount的值为0,并释放锁
  • 此时线程二继续执行,但是线程二已经进入while循环了,因此线程二也将执行一次减法操作,故而就会出现ticketCount=-1的情况

因此修正后的代码应该是

cpp 复制代码
void sellTicket(int idx)
{
    while(ticketCount>0)
    {
        mtx.lock();
        if(ticketCount>0)
        {
            cout<<ticketCount<<endl;
            ticketCount--;
        }
        mtx.unlock();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms
    }
}

lock_guard

由于mutex需要程序员时刻记住在何时加锁在何时释放锁,否则就会导致死锁问题,但大多数时候这个工作比较繁琐,并且很容易忘记释放锁,因此出现了lock_guard,可自动管理加锁和解锁

  • std::lock_guard 是 C++11 提供的 RAII(资源获取即初始化)风格的锁管理工具,用于自动管理 std::mutex 的加锁和解锁操作。
  • 通过 std::lock_guard,可以在作用域内自动锁定 std::mutex,并在作用域结束时自动释放锁,从而避免忘记手动解锁或异常情况下未能正确解锁互斥量。
  • std::lock_guard 的构造函数接受一个 std::mutex 对象,并在构造时锁定该互斥量,在析构时释放锁。因此,使用 std::lock_guard 可以很方便地实现线程安全的代码块。
cpp 复制代码
void sellTicket(int idx)
{
    while(ticketCount>0)
    {
        // mtx.lock();
        {
            lock_guard<mutex> lock(mtx);
            if(ticketCount>0)
            {
                cout<<ticketCount<<endl;
                ticketCount--;
            }
        }
        // mtx.unlock();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms
    }
}
相关推荐
小小小小钰儿2 天前
2.3-云端API集成
人工智能·计算机·网络安全·操作系统·编程
做系统的大强2 天前
脚本说 PASS、OS 读全零 —— 到底是谁在撒谎?
后端·操作系统
小小小小钰儿2 天前
2.2-模型微调
人工智能·深度学习·机器学习·计算机·网络安全·操作系统·编程
小小小小钰儿3 天前
2.1-Windows本地部署大模型
人工智能·windows·计算机·网络安全·操作系统·编程
流浪0013 天前
Linux系统篇39——线程(四) pthread库的由来和线程的创建与等待
linux·操作系统·线程·pthread库
流浪0014 天前
Linux系统篇42——线程(七) pthread库管理线程的工作流,从创建到回收
linux·操作系统·线程·线程创建到销毁
云计算练习生4 天前
什么是 Linux?从 Unix 到开源生态的完整脉络
linux·开源·操作系统·unix
云计算练习生5 天前
什么是上下文切换?进程切换时 CPU 到底在保存什么
linux·服务器·操作系统·进程切换
CS创新实验室6 天前
为什么三台机器就能选出一个“带头大哥“?——Raft 一致性算法,一次讲透
分布式·操作系统
sweet丶6 天前
iOS 为什么没有内存交换分区(swap)
操作系统