从汇编角度解释线程间互斥-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
    }
}
相关推荐
WZF-Sang17 小时前
Linux—进程学习-01
linux·服务器·数据库·学习·操作系统·vim·进程
Goboy1 天前
0帧起步:3分钟打造个人博客,让技术成长与职业发展齐头并进
程序员·开源·操作系统
结衣结衣.1 天前
【Linux】Linux管道揭秘:匿名管道如何连接进程世界
linux·运维·c语言·数据库·操作系统
OpenAnolis小助手2 天前
龙蜥副理事长张东:加速推进 AI+OS 深度融合,打造最 AI 的服务器操作系统
ai·开源·操作系统·龙蜥社区·服务器操作系统·anolis os
小蜗的房子3 天前
SQL Server 2022安装要求(硬件、软件、操作系统等)
运维·windows·sql·学习·microsoft·sqlserver·操作系统
邂逅岁月4 天前
【多线程奇妙屋】 Java 的 Thread类必会小技巧,教你如何用多种方式快速创建线程,学并发编程必备(实践篇)
java·开发语言·操作系统·线程·进程·并发编程·javaee
CXDNW6 天前
【系统面试篇】进程和线程类(1)(笔记)——区别、通讯方式、同步、互斥、死锁
笔记·操作系统·线程·进程·互斥·死锁
Anemone_6 天前
MIT 6.S081 Lab3
操作系统
掘了7 天前
持久化内存 | Persistent Memory
c++·架构·操作系统
结衣结衣.8 天前
【Linux】掌握库的艺术:我的动静态库封装之旅
linux·运维·服务器·c语言·操作系统·