文章目录
- [1. 封装简易线程并模拟抢票](#1. 封装简易线程并模拟抢票)
- [2. 线程互斥--->给资源上锁解决抢票bug](#2. 线程互斥--->给资源上锁解决抢票bug)
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--这个动作,在汇编层面是三条指令:
- LOAD -> 把内存里的
tickets值读到CPU寄存器中 - DEC -> 在寄存器中把值减一
- STORE -> 把寄存器里的新值写回内存
假设现在tickets == 1
- 时刻1 线程A: 线程A
if判断通过,LOAD把1读到自己的寄存器 - 时刻2 线程切换: 线程A时间片耗尽/睡觉了
usleep- 重点:线程A的寄存器保存的是1,线程A停在这了,内存里的
tickets依然是1
- 重点:线程A的寄存器保存的是1,线程A停在这了,内存里的
- 时刻3 线程B: 线程B看到
tickets还是1,通过if判断 - 时刻4 线程B: 线程B一口气跑完:读1、减1、写回0
- 此时内存里的
tickets变成0
- 此时内存里的
- 时刻5 线程A切回: 继续执行剩下的命令:减1、写回0--->0被写了两次
- 注意:如果是单纯的原子性问题,这里是重复更新。但如果是更加复杂的逻辑,其他线程进来,会把票数剪成负数!
总结:
tickets--不是原子操作,在CPU层面对应三条指令:LOADDECSTORE,如果线程A在LOAD之后、STORE之前被切走,它的寄存器值就停留在那一刻,等恢复时,会用旧值计算并覆盖内存,导致其他的线程修改操作被覆盖或者在逻辑检查之后导致数据越界
2. 线程互斥--->给资源上锁解决抢票bug
在 Linux 原生线程库中,这把锁叫做 互斥量 (Mutex),全称 Mutual Exclusion。
它的工作原理非常简单:一把锁,只有一个钥匙。谁抢到钥匙谁进屋(临界区),没抢到的在门口排队(阻塞)
核心API
买锁 (初始化) :
pthread_mutex_init上锁 (加锁) :
pthread_mutex_lock(原子操作,抢不到就睡觉)开锁 (解锁) :
pthread_mutex_unlock(原子操作,唤醒排队的人)扔锁 (销毁) :
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),保证了读取-修改-回写这一连串操作在硬件层面是不会被打断的
