基于抢票系统的线程互斥详解

文章目录

1. 封装简易线程并模拟抢票

封装简易线程

版本一:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <unistd.h>

namespace thread_v1
{
    static int gnumber = 1;
    using func_t = std::function<void()>;
    enum STATUS
    {
        NEW = 0, // 刚创建 还没运行
        RUNNING, // 正在运行
        STOP     // 停止
    };

    class thread
    {
    private:
        // 为什么要static?
        // pthread_create要求传一个普通的函数指针
        // 但是类内成员函数天然有一个this指针 定义在类外不优雅
        // 而静态成员函数没有this指针!
        static void *routine(void *arg)
        {
            // 参数传完了 再强转回对象原来的类型
            thread *t = static_cast<thread *>(arg);
            t->_status = STATUS::RUNNING;
            // 回调真正的业务逻辑
            t->_func();
            return nullptr;
        }
        void EnableDetach() { _joinable = false; }

    public:
        thread(func_t func)
            : _func(func), _status(STATUS::NEW), _joinable(true) // 默认是可连接
        {
            _name = "thread- " + std::to_string(gnumber++);
            _pid = getpid();
        }

        bool Start()
        {
            // 只有NEW状态下才能启动
            if (_status != STATUS::RUNNING)
            {
                int n = ::pthread_create(&_tid, nullptr, routine, this);
                if (n != 0)
                    return false;

                return true;
            }
            return false;
        }

        bool Stop()
        {
            if (_status == STATUS::RUNNING)
            {
                int n = ::pthread_cancel(_tid);
                if (n != 0)
                    return false;

                _status = STATUS::STOP;
            }
            return false;
        }

        bool Join()
        {
            // 只有处于joinable状态才能join
            if (_joinable)
            {
                int n = ::pthread_join(_tid, nullptr);
                if (n != 0)
                    return false;

                _status = STATUS::STOP;
                return true;
            }
            return false;
        }

        void Detach()
        {
            EnableDetach();
            pthread_detach(_tid);
        }

        bool IsJoinable() const { return _joinable; }
        std::string getName() const { return _name; }
        ~thread() {}

    private:
        pthread_t _tid;
        std::string _name;
        pid_t _pid;
        bool _joinable;
        func_t _func;
        STATUS _status;
    };
}

版本二:

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <functional>

namespace thread_v2
{
    static int gnumber = 1;
    enum STATUS
    {
        NEW,
        RUNNING,
        STOP
    };

    template <class T>
    class thread
    {
        using func_t = std::function<void(T)>;
    private:
        static void *routinue(void *arg)
        {
            thread<T> *t = static_cast<thread<T> *>(arg);
            t->_status = STATUS::RUNNING;
            t->_func(t->_data);
            return nullptr;
        }
        void EnableJoinable() { _joinable = false; }

    public:
        thread(func_t func, T data)
            : _func(func), _status(STATUS::NEW), _joinable(true), _pid(getpid()),_data(data)
        {
            _name = "thread_v2-" + std::to_string(gnumber++);
        }

        bool Start()
        {
            if (_status != STATUS::RUNNING)
            {
                int n = ::pthread_create(&_tid, nullptr, routinue, this);
                if (n != 0)
                    return false;

                _status = STATUS::RUNNING;
                return true;
            }
            return false;
        }

        bool Stop()
        {
            if (_joinable)
            {
                int n = ::pthread_join(_tid, nullptr);
                if (n != 0)
                    return false;

                _status = STATUS::STOP;
                _joinable = false;
                return true;
            }
            return false;
        }

        bool Join() { return Stop(); }

        void Detach()
        {
            EnableJoinable();
            pthread_detach(_tid);
        }

        std::string getName() const { return _name; }
        bool IsJoinable() const { return _joinable; }
        ~thread() {}

    private:
        pthread_t _tid;
        std::string _name;
        STATUS _status;
        func_t _func;
        pid_t _pid;
        bool _joinable;
        T _data;
    };
}

模拟抢票

利用封装的版本一模拟抢票环节:

cpp 复制代码
#include "thread_v1.hpp"
using namespace std;
using namespace thread_v1;

// 模拟抢票
using namespace std;
using namespace thread_v1;

// 全局共享资源 火车票
static int tickets = 1000;

void GetTicket()
{
    while (1)
    {
        if (tickets > 0)
        {
            // 模拟抢票耗时
            usleep(1000);

            cout << "当前线程: " << pthread_self() << "抢到了第 " << tickets << " 张票\n";
            tickets--;
        }else{
            break;
        }
    }
}

int main(){
    thread t1(GetTicket);
    thread t2(GetTicket);
    thread t3(GetTicket);
    thread t4(GetTicket);

    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();

    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();

    cout << "最终剩余票数: " << tickets << endl;

    return 0;
}

**票数为什么会成负数!?**代码里明明有if(tickets > 0)的判断!

原子性和汇编视角解析bug

铺垫一个概念:原子性

要么全做,要么全不做->类似bool只有1/0两态--->执行时不可能被别人看到或打断

对于ticket--这个动作,在汇编层面是三条指令:

  1. LOAD -> 把内存里的tickets值读到CPU寄存器中
  2. DEC -> 在寄存器中把值减一
  3. STORE -> 把寄存器里的新值写回内存

假设现在tickets == 1

  1. 时刻1 线程A: 线程Aif判断通过,LOAD把1读到自己的寄存器
  2. 时刻2 线程切换: 线程A时间片耗尽/睡觉了usleep
    • 重点:线程A的寄存器保存的是1,线程A停在这了,内存里的tickets依然是1
  3. 时刻3 线程B: 线程B看到tickets还是1,通过if判断
  4. 时刻4 线程B: 线程B一口气跑完:读1、减1、写回0
    • 此时内存里的tickets变成0
  5. 时刻5 线程A切回: 继续执行剩下的命令:减1、写回0--->0被写了两次
    • 注意:如果是单纯的原子性问题,这里是重复更新。但如果是更加复杂的逻辑,其他线程进来,会把票数剪成负数!

总结:

tickets--不是原子操作,在CPU层面对应三条指令:LOAD DEC STORE,如果线程A在LOAD之后、STORE之前被切走,它的寄存器值就停留在那一刻,等恢复时,会用旧值计算并覆盖内存,导致其他的线程修改操作被覆盖或者在逻辑检查之后导致数据越界

2. 线程互斥--->给资源上锁解决抢票bug

在 Linux 原生线程库中,这把锁叫做 互斥量 (Mutex),全称 Mutual Exclusion。

它的工作原理非常简单:一把锁,只有一个钥匙。谁抢到钥匙谁进屋(临界区),没抢到的在门口排队(阻塞)

核心API

  1. 买锁 (初始化) : pthread_mutex_init

  2. 上锁 (加锁) : pthread_mutex_lock (原子操作,抢不到就睡觉)

  3. 开锁 (解锁) : pthread_mutex_unlock (原子操作,唤醒排队的人)

  4. 扔锁 (销毁) : pthread_mutex_destroy

代码修复--->手动上锁解锁

调用版本一:

cpp 复制代码
using namespace std;

static int tickets = 1000;
// 定义一把全局锁
pthread_mutex_t mtx;

void GetTicket(){
    while(1){
        // 1. 加锁
        // 排队领钥匙,如果这时候被人拿着锁,当前线程阻塞
        pthread_mutex_lock(&mtx);

        // =========== 临界区开始 只有一个人能进入 ==========
        if(tickets > 0){
            usleep(1000);
            cout << "当前线程: " << pthread_self() << " 抢到了第 " << tickets << " 张票\n";
            tickets--;

            // 2. 解锁
            // 办完事了,赶紧还钥匙,让门口排队的人抢
            pthread_mutex_unlock(&mtx);
        } else {
            // 解锁完再走!!!!
            // 如果没票 准备break离开循环
            // 但是手里还拿着锁呢!!!直接break 这把锁永远不会释放!!!
            // 结果:门口的人等到天荒地老!死锁!
            pthread_mutex_unlock(&mtx);
            break;
        }
        // =========== 临界区结束 ===================
        // 抢完一张票后面排队去,给别人机会,不然一个线程可能解锁之后又抢到了,导致线程饥饿
        usleep(1000);
    }
}

int main(){
    // 0. 初始化锁
    // nullptr表示默认属性
    pthread_mutex_init(&mtx, nullptr);

    thread_v1::thread t1(GetTicket);
    thread_v1::thread t2(GetTicket);
    thread_v1::thread t3(GetTicket);
    thread_v1::thread t4(GetTicket);

    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();

    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();

    // 3. 销毁锁
    // 所有人都走了之后再销毁
    pthread_mutex_destroy(&mtx);

    cout << "最终剩余票数: " << tickets << endl;

    return 0;
}

封装锁并修复抢票系统

调用版本一,需要手动解锁,很容易遗忘,现在可以把锁封装成一个对象,进行自动管理

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

// 1. 封装底层的互斥锁
// 出生初始化锁 销毁扔掉锁
class Mutex{
public:
    // 禁止拷贝
    // 锁是一种独占的系统资源,拷贝会导致所有权不明,可能会二次析构或二次解锁
    Mutex(const Mutex&) = delete;
    Mutex& operator=(const Mutex&) = delete;

    Mutex(){ ::pthread_mutex_init(&_lock, nullptr); }
    void Lock() { ::pthread_mutex_lock(&_lock); }
    void Unlock() { ::pthread_mutex_unlock(&_lock); }
    ~Mutex() { ::pthread_mutex_destroy(&_lock); }

private:
    pthread_mutex_t _lock;
};

// 2. 封装锁的守卫 RAII
// 创建自动上锁 销毁自动解锁
class LockGuard{
private:
    Mutex& _mutex;  // 要操作锁 而不是操作拷贝的锁
public:
    LockGuard(Mutex& mutex) :_mutex(mutex) { _mutex.Lock(); }
    ~LockGuard() { _mutex.Unlock(); }
};

修复抢票系统:

cpp 复制代码
// RAII加锁
#include "LockGuard.hpp"

using namespace std;

// 1. 定义共享数据结构
// 锁和资源绑定
struct TicketData{
    int tickets;
    Mutex mtx;
    string name;
    
    TicketData(int t, string n)
        :tickets(t)
        ,name(n)
        {}
};

// 2. 抢票逻辑
void GetTicket(TicketData* data){
    while(1){
        // 作用域块:一个局部遍历区域,用{}把临界区包起来
        {
            // 1. 构造guard对象 自动调用mtx.Lock()
            LockGuard guard(data->mtx);

            if(data->tickets > 0){
                usleep(1000);
                cout << "线程 " << pthread_self() << " (" << data->name
                     << ") 抢到了第 " << data->tickets << " 张票\n";
                data->tickets--;
            }else{
                // guard对象离开作用域,析构自动调用mtx.Unlock()
                break;
            }
        }
        usleep(1000);
    }
    cout << "线程 " << pthread_self() << " 退出抢票...\n";
}

int main(){
    // 准备共享数据
    // 数据必须在堆上或者主线程栈上,保证声明周期比子线程更长
    TicketData* global_data = new TicketData(1000,"抢票系统");

    // T被推到为TicketData*
    thread_v2::thread<TicketData*> t1(GetTicket, global_data);
    thread_v2::thread<TicketData*> t2(GetTicket, global_data);
    thread_v2::thread<TicketData*> t3(GetTicket, global_data);
    thread_v2::thread<TicketData*> t4(GetTicket, global_data);

    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();

    t1.Stop();
    t2.Stop();
    t3.Stop();
    t4.Stop();

    cout << "最终剩余票数: " << global_data->tickets << endl;

    delete global_data;
    return 0;

}

锁的原理

锁的本质:原子指令

在汇编层面,普通的int a = 0或者if(a == 0)都不是原子的。CPU提供了特殊的原子指令,比如 Test-And-Set (TAS) 或者 Compare-And-Swap (CAS) ,以及 x86 下的 xchg(交换)指令

伪汇编代码演示(以 xchg 锁为例): 假设内存中有一个变量 mutex,0代表空闲,1代表被锁:

assembly 复制代码
lock:
    mov  al, 1      ; 把寄存器 al 设为 1
    xchg al, mutex  ; 【关键一步】原子交换寄存器al和内存mutex的值
    ; 这一步由硬件保证,哪怕有100个CPU核同时执行,
    ; 总线仲裁器也只允许一个先执行,绝对不会同时发生。
    
    test al, al     ; 检查 al 里的旧值
    jnz  suspend    ; 如果 al 原来就是 1(说明别人锁着呢),我就挂起(去排队)
    ret             ; 如果 al 原来是 0,现在内存里变成 1 了,且我拿到了 0,加锁成功!

unlock:
    mov  mutex, 0   ; 简单的把内存改回 0
    ret             ; 唤醒排队的人

互斥锁的底层依赖于CPU提供的原子指令(如x86的xchg),保证了读取-修改-回写这一连串操作在硬件层面是不会被打断的

相关推荐
是个西兰花2 小时前
进程间通信:匿名管道
linux·运维·服务器
小北方城市网2 小时前
Spring Cloud Gateway 生产级微内核架构设计与可插拔过滤器开发
java·大数据·linux·运维·spring boot·redis·分布式
wacpguo2 小时前
Ubuntu 24.04 安装 Docker
linux·ubuntu·docker
Lenyiin2 小时前
Linux 进程控制
linux·运维·服务器
春日见2 小时前
Git 相关操作大全
linux·人工智能·驱动开发·git·算法·机器学习
述清-架构师之路3 小时前
vmWare的CentOS系统网路连不上处理记录
linux·运维·centos
郝学胜-神的一滴3 小时前
Linux网络字节序详解:从理论到实践
linux·服务器·c语言·开发语言·c++·网络协议·程序人生
实心儿儿3 小时前
Linux —— 进程概念 - 僵尸进程、孤儿进程
linux·运维·服务器
Trouvaille ~3 小时前
【Linux】线程概念与控制(一):线程本质与虚拟地址空间
linux·运维·服务器·c++·线程·虚拟地址空间·pcb