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 架构下的汇编指令为例,该语句一般会转换成三条汇编语句(非优化条件下),
- 将变量
tickets
的值从内存加载到寄存器中
一般会使用类似mov
指令(move
的缩写,用于数据传送)把内存中的值取到寄存器里,例如使用eax
寄存器(这只是一种常见选择,不同编译器可能选用不同寄存器):
mov eax, dword ptr [tickets] ; 将内存中tickets变量的值(假设是32位整型,所以用dword ptr)加载到eax寄存器
这里 dword ptr
表示操作的数据大小是双字(32位),也就是对应一个32位的整型变量,[tickets]
表示取 tickets
这个变量所对应的内存地址处的值。
- 对寄存器中的值进行自减操作
使用sub
指令(subtract
的缩写,用于减法运算)来实现自减,例如:
sub eax, 1 ; 在eax寄存器中的值(也就是之前加载的tickets的值)减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