线程互斥与锁机制详解
目录
一:情景引入
二:进程线程间的互斥相关概念
三:互斥量mutex
四:互斥量的接口
4.1初始化互斥量
4.2销毁互斥量
4.3互斥量加锁和解锁
4.4锁的使用
五:互斥量实现原理探究
六:锁的封装
一:情景引入
在生活中,常见的一种现象是抢票,抢名额,等抢一些较少的资源。以抢票为例:
下面是四个线程模拟四个人抢1000张票
进程什么时候会切换:
1.时间片耗尽
2.更高优先级的进程要调度
3.用过sleep,然后从内核返回用户,会进行时间片是否到达的检测,进而导致切换
在我们所写的抢票函数中,已经标明了进入函数内部要求现有的票数大于0,为什么最后的结果还是出现了负数呢?
原因有:
1.ticketnum--不是原子操作,那什么是原子操作呢?
2.Ticket函数中的if判断条件是计算,需要将数据从内存放到CPU中计算,与这个有什么关系呢?
带着这两个问题,了解学习互斥量
二:进程线程间的互斥相关概念
共享资源
临界资源:多线程执行流被保护的共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起 保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成, 要么未完成
三:互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量 归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完 成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。例如上述抢票情境下,出现票为负数的情况:
1.if 语句判断条件为真以后,代码可以并发的切换到其他线程,而且if的判断条件是逻辑运算,会把数据从内存放到寄存器中,内存是多线程共享的,但是CPU寄存器的上下文是线程私有的,此时一个线程对于ticknum的--结果在加载到内存之前,其他线程是不知道的,进程看到的还是--之前的数字
2.usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
3.ticket -- 操作本身就不是一个原子操作
-- 操作并不是原子操作,而是对应三条汇编指令:
load :将共享变量ticket从内存加载到寄存器中
update :更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
1.代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
2.如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程 进入该临界区。
3.如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
1.所有对于资源的保护,都是对临界区的保护,因为资源都是通过代码访问的
2.加锁,一定不能大块的加锁,要保证细颗粒度
3.加锁就是找临界区,对临界区加锁
四:互斥量的接口
4.1初始化互斥量
初始化互斥量有两种方法:
方法1:静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2:动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);参数:mutex:要初始化的互斥量attr:NULL
4.2销毁互斥量
销毁互斥量需要注意:
使用PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
4.3互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
lock:加锁成功,访问临界资源,加锁失败,阻塞等待
trylock:加锁成功,访问临界资源,加锁失败,返回错误码
调用 pthread_ lock 时,可能会遇到以下情况:
1.互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
2.发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到 互斥量,那么pthread_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
4.4锁的使用
需要注意的是:所以的线程都要看向同一把锁
对线程的简单封装:
// 模版
namespace ThreadModule
{// 原子计数器,方便形成线程名称static int num = 1;enum class TSTATUS{NWE,RUNNING,STOP};template <typename T>class Thread{using func_t = std::function<void(T&)>;private:// 如果不写成静态成员变量,默认一个参数是this,不符合pthread_creat参数要求// 如果写到类外,类的封装性收到破坏static void *Routine(void *args){Thread *t = static_cast<Thread *>(args);t->_status = TSTATUS::RUNNING;t->_func(t->_data);return nullptr;}void EnableDetach() { _joinable = false; };public:Thread(func_t func,T& data) : _func(func), _joinable(true), _status(TSTATUS::NWE),_data(data){_name = "Thread" + std::to_string(num++);_pid = getpid();};bool Start(){if (_status != TSTATUS::RUNNING){int n = pthread_create(&_tid, nullptr, Routine, this);if (n != 0)return false;return true;}return false;}bool Stop(){if (_status == TSTATUS::RUNNING){int n = pthread_cancel(_tid);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}bool Join(){if (_joinable){int n = pthread_join(_tid, nullptr);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}void Detach(){EnableDetach();pthread_detach(_tid);}bool IsJoinable(){return _joinable;}std::string Name(){return _name;}~Thread() {};private:std::string _name;pid_t _pid;pthread_t _tid;func_t _func;bool _joinable; // 判断是否分离,默认不是TSTATUS _status;T _data;};}
using namespace ThreadModule;
int ticketnum = 10000;
//pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
#define NUM 4class ThreadData
{
public:std::string name;pthread_mutex_t *lock_ptr;
};void Ticket(ThreadData &td)
{// 使用共享资源的代码块while (true){pthread_mutex_lock(td.lock_ptr);if (ticketnum > 0){//usleep(1000);// 1.抢票printf("get a new ticket : id : %d\n", ticketnum--);// 2.入库模拟// usleep(1000);pthread_mutex_unlock(td.lock_ptr);}else{pthread_mutex_unlock(td.lock_ptr);break;}}
}
int main()
{pthread_mutex_t lock;pthread_mutex_init(&lock,nullptr);// 1. 构建线程对象std::vector<Thread<ThreadData>> threads;for (int i = 0; i < NUM; i++){ThreadData *td = new ThreadData();td->lock_ptr = &lock;//所有的线程要看向同一把锁// 直接在 threads 向量的末尾构造一个 Thread<ThreadData> 对象构造时使用提供的参数(Ticket 函数和 *td 对象)// 避免了先创建临时 Thread 对象再拷贝 / 移动到容器中的开销// 和push_back类似,但 emplace_back 更加高效threads.emplace_back(Ticket, *td);td->name = threads.back().Name();}// 2. 启动线程for (auto &thread : threads){thread.Start();}// 3. 等待线程for (auto &thread : threads){thread.Join();}return 0;
}
通过代码可以看出,加锁之后,可以保证临界区只有一个线程使用,结果就是正确的
五:互斥量实现原理探究
经过上面的例子,我们已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和 内存单元的数据相交换,由于只有⼀条指令,保证了原子性,即使是多处理器平台,访问内存的总线周 期也有先后,一个处理器上的交换指令执行时另⼀个处理器的交换指令只能等待总线周期。
现在 我们把lock和unlock的伪代码改一下
锁本身是全局的,那么锁也是共享资源!谁保证锁的安全??之后文章会讲解
如何看待锁呢?二元信号量就是锁!
1. 加锁本质就是对资源展开预订!
2. 整体使用资源!!
如果申请锁的时候,锁被别人已经拿走了,怎么办?其他线程要进行阻塞等待
线程在访问临界区代码的时候,可以不可以切换??可以切换!!
我被切走的时候,别人能进来吗??不能!我是抱着锁,被切换的!!不就是串行吗!效率低的原因!原子性!
六:锁的封装
互斥锁:不允许拷贝,赋值,要保证多个线程看到同一把锁
#pragma once#include <iostream>
#include <string>
#include <pthread.h>namespace MutexModule
{class Mutex{public:// 删除不要的拷⻉和赋值Mutex(const Mutex &) = delete;const Mutex &operator=(const Mutex &) = delete;Mutex(){int n = pthread_mutex_init(&_mutex, nullptr);(void)n;}void Lock(){int n = pthread_mutex_lock(&_mutex);(void)n;}void Unlock(){int n = pthread_mutex_unlock(&_mutex);(void)n;}pthread_mutex_t *GetMutexOriginal() // 获取原始指针{return &_mutex;}~Mutex(){int n = pthread_mutex_destroy(&_mutex);(void)n;}private:pthread_mutex_t _mutex;};// 进⾏锁管理class LockGuard{public:LockGuard(Mutex &mutex) : _mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex;//只有一把锁,传锁的引用};}
#include <iostream>
#include <vector>
#include "Thread.hpp"
#include "Mutex.hpp"using namespace ThreadModule;
using namespace MutexModule;int ticketnum = 10000;
Mutex mutex;
#define NUM 4class ThreadData
{
public:std::string name;
};void Ticket(ThreadData &td)
{// 使用共享资源的代码块while (true){//临时变量,处理作用域就会销毁//这个类的初始化就是加锁,销毁就是解锁,实现对锁的保护LockGuard guard(mutex);if (ticketnum > 0){//usleep(1000);// 1.抢票printf("get a new ticket : id : %d\n", ticketnum--);// 2.入库模拟// usleep(1000);}else{break;}}
}
int main()
{// 1. 构建线程对象std::vector<Thread<ThreadData>> threads;for (int i = 0; i < NUM; i++){ThreadData *td = new ThreadData();// 直接在 threads 向量的末尾构造一个 Thread<ThreadData> 对象构造时使用提供的参数(Ticket 函数和 *td 对象)// 避免了先创建临时 Thread 对象再拷贝 / 移动到容器中的开销// 和push_back类似,但 emplace_back 更加高效threads.emplace_back(Ticket, *td);td->name = threads.back().Name();}// 2. 启动线程for (auto &thread : threads){thread.Start();}// 3. 等待线程for (auto &thread : threads){thread.Join();}return 0;
}