【Linux系统编程】:互斥量和线程安全

1. 前言

在学习线程互斥前,我们应该了解一下几个概念,

  • 临界资源:是指在一段时间内,一次只能被一个进程或线程访问的资源。
  • 临界区:是指在多进程或多线程程序中,访问临界资源(一次只能被一个进程或线程访问的资源)的代码段。
  • 原子性:是指一个操作或者一系列操作要么全部执行,要么全部不执行,不会出现部分执行的情况。就好像原子是物质的基本单位,不可再分割一样,具有原子性的操作是一个不可分割的整体。
  • 在并发编程领域,互斥是一种机制,用于确保在同一时刻只有一个进程或线程能够访问共享资源(如临界资源)或执行特定的代码段(如临界区)。

2. 多线程简略模拟售票系统

我们用多线程模拟用户抢票的过程,

#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <pthread.h>
#include <unistd.h>

using namespace std;
int tickets = 1000; // 用多线程,模拟一轮抢票
#define NUM 4       // 线程数量
class threadData
{
   
public:
    threadData(int number)
    {
   
        threadname = "thread_" + to_string(number);
    }

public:
    string threadname;
};
void *getTicket(void *arg)
{
   
    threadData *td = static_cast<threadData *>(arg);
    const char *name = td->threadname.c_str();
    while (true)
    {
   
        if (tickets > 0)
        {
   
            usleep(1000);
            printf("this is %s get ticket:%d\n", name, tickets);
            tickets--;
        }
        else
            break; 
    }

    printf("%s ...quit\n", name);
    return nullptr;
}
int main()
{
   
    vector<pthread_t> tids;            // 线程id表
    vector<threadData *> thread_datas; // 线程数据表
    // 创建多线程
    for (int i = 1; i <= NUM; i++)
    {
   
        pthread_t tid;
        threadData *td = new threadData(i);
        thread_datas.push_back(td);
        pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
        tids.push_back(tid);
    }
    // 回收多线程
    for (auto &tid : tids)
    {
   
        pthread_join(tid, nullptr);
    }
    // 释放内存
    for (auto &td : thread_datas)
    {
   
        delete td;
    }
}

编译运行,
在这里插入图片描述

2.1 问题1:tickets–是安全的吗?

tickets–,假设是从1000减到999,以常见的 x86 架构下的汇编指令为例,该语句一般会转换成三条汇编语句(非优化条件下),

  1. 将变量 tickets 的值从内存加载到寄存器中
    一般会使用类似 mov 指令(move 的缩写,用于数据传送)把内存中的值取到寄存器里,例如使用 eax 寄存器(这只是一种常见选择,不同编译器可能选用不同寄存器):
mov eax, dword ptr [tickets]  ; 将内存中tickets变量的值(假设是32位整型,所以用dword ptr)加载到eax寄存器

这里 dword ptr 表示操作的数据大小是双字(32位),也就是对应一个32位的整型变量,[tickets] 表示取 tickets 这个变量所对应的内存地址处的值。

  1. 对寄存器中的值进行自减操作
    使用 sub 指令(subtract 的缩写,用于减法运算)来实现自减,例如:
sub eax, 1  ; 在eax寄存器中的值(也就是之前加载的tickets的值)减1
  1. 将寄存器中自减后的值写回内存中的 tickets 变量所在位置
    再用 mov 指令把寄存器里更新后的值存回内存,以更新 tickets 变量的值:
mov dword ptr [tickets], eax  ; 将eax寄存器中自减后的结果写回tickets变量对应的内存地址处``

整体来看,在未优化时大致对应的汇编指令序列就是这样:
```asm
mov eax, dword ptr [tickets]
sub eax, 1
mov dword ptr [tickets], eax

在这里插入图片描述
假设thread_1正在执行第一步将数据从内存加载到寄存器,此时thread_2突然被调度,thread_1保存上下文并被挂起,thread_2被执行,由于内存中中的tickets没有被修改,所以thread_2执行时tickets是1000,假设thread_2在自己的时间片中(假设出现这一极端情况)一直运行,将内存中的tickets减到800,并再次被CPU调度,而这次调度执行到第2步时,thread_1被执行了,于是切换CPU中的上下文,继续执行未执行的第二步,此时CPU中寄存器中tickets的值是1000,经过第三步,内存中的tickets的值变为999,就造成数据不一致问题。所以,对全局变量进行多线程++/–如果不加以保护是不安全的。(怎么保护,后面讲解)

2.2 问题2:为什么tickets会变为负数?

豆包的回答:
在多线程环境下,多个线程同时执行getTicket函数,它们都会访问并修改同一个全局变量tickets。
假设当前tickets的值为 1,线程 A 和线程 B 几乎同时执行到if (tickets > 0)这一判断语句,由于操作系统的线程调度机制,在极短时间内,两个线程都判断此时tickets大于 0(因为还没来得及被其他线程修改),然后线程 A 执行usleep(1000)暂停了 1 毫秒,而在这 1 毫秒内,线程 B 没有被暂停,它继续执行,先输出抢到票的信息然后将tickets减 1,此时tickets变为 0 了。
接着线程 A 的usleep(1000)暂停结束,它继续往下执行,由于之前已经判断过tickets > 0(当时确实大于 0)&#x

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值