线程互斥
我们知道线程之间是共享地址空间的,也就是共享资源,这很容易造成数据不一致问题,所以引入线程的互斥来解决数据不一致问题。
相关概念
临界资源:多线程执行流共享的资源叫做临界资源。
临界区:每个线程内部,访问临界资源的代码交临界区。
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区访问临界资源,保护临界资源。
原子性:不被任何调度机制打断的操作,该操作只有两态,要么"真",要么"假"。
理解原子性
前面我们也讲过做语言层做基本运算,需要如下操作。

比如我们要对变量,做--操作:
第一步:把变量从物理内存加载到cpu。
第二步:在cpu中对变量做计算。
第三步:把计算好的数据写回物理内存中。
这个减减操作在底层起始都是通过寄存器来对变量进行操作的。
eg:只是简单举例
move ebx ticket
减少 ebx 1
写回 地址 ebx
另一方面我们知道cpu具有调度时间,当一个线程在cpu上运行时间片过之后,cpu会保存该线程的运行上下文,接着切换到另一个线程,如果这样不加保护很容易照成数据不一致问题。

所以我们上边概念介绍到不被任何调度机制打断的操作成为原子性,像这种底层对应有多条汇编指令的操作并不能称之为原子性。所以自然可以得到,单条汇编指令完成的操作就可以称之为原子性。因为他不会被任何调度机制所打断。
**注:**这里小编只是觉得能够更好的理解原子性的特性。
案例引入
先看问题
代码主要功能:创建4个线程,让这四个线程不断的去对全局变量做减减操作。
cpp
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 创建多个进程然后同时对一个变量操作
int ticket = 100;
void *routine(void *args)
{
string s = static_cast<const char *>(args);
while (true)
{
if (ticket > 0)
{
usleep(1000);
cout << s << "sells" << ticket << endl;
ticket--;
}
else
{
break;
}
}
return nullptr;
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, routine, (void *)"thread-1");
pthread_create(&t2, nullptr, routine, (void *)"thread-1");
pthread_create(&t3, nullptr, routine, (void *)"thread-1");
pthread_create(&t4, nullptr, routine, (void *)"thread-1");
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
return 0;
}
//运行结过
/*
thread-4:ticket:100
thread-3:ticket:99
thread-4:ticket:98
thread-3:ticket:97
thread-2:ticket:96
thread-4:ticket:95
thread-3:ticket:94
.
.
.
thread-1:ticket:4
thread-4:ticket:3
thread-2:ticket:2
thread-1:ticket:1
thread-3:ticket:0
thread-4:ticket:-1
thread-2:ticket:-2
*/
在代码中我们对变量ticket做了控制(if(ticket>0)),但是最后运行结果还是减到了负数。这就是该程序存在数据不一致问题。
上边讲的ticket--非原子性会造成ticket的值变大,而造成ticket变为负数的原因是因为判断逻辑+usleep()函数。

我们对没有保护的共享资源进行访问时,往往会造成数据不一致问题。通过加锁就能解决问题。

穿插一个相关问题
多线程有更多的并发执行流,也有更多的线程切换。那在新线程切换的时间点是什么时候?
我们知道对于线程调度的时间片结束/sleep/io阻塞时都可能会进行切换,其本质就是陷入内核态。
所以当cpu调度切换选择新的线程时,是在从内核态返回用户态的时候进行检查切换。这也是为什么我们使用usleep时会照成数据不一致其中原因之一(我们主动的让线程更有大几率陷入内核,更改线程)。
互斥量/锁(Mutex)
为了解决多线程中访问临界资源看到数据不一致的问题,我们可以通过对临界资源进行加锁操作,Linux下把这把锁称之为互斥量。
因此有了锁的概念我可以把加锁区域称之为临界区,非加锁部分称之为非临界区。对于非临界区多个执行流可以并发执行,对于临界区之云希一共线程执行流去执行。对临界资源进行加锁起始就是对临界区代码进行加锁。

互斥量接口
互斥量的初始化
方法一:全局变量初始化
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER
方法二:接口初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
互斥量的销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意
使用PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁,程序结束锁自动释放
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
互斥量加锁/解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁
返回值:成功返回0,失败返回错误号
锁的相关规则
1.申请的锁:多线程竞争这把锁,本质也是临界资源,必须是原子性的。
成功申请到锁,访问临界区代码/访问临界资源。
申请失败:阻塞挂起,申请执行流。申请失败是因为有其他进程正在使用锁,所以站在其他线程角度对于这个锁要么能用要么不能用,本质也是原子的。
2.锁功能本质:锁的本质就是让执行 临界区的代码有并行改为串行。
3.加锁规则:尽量加锁的范围力度要比较细,尽可能不要包含太多的非临界区。
4.加锁之后在临界区内部允许线程切换,因为当前线程并没有释放锁,当线程切换时会连同锁一切被切换,只有等该线程再次被调度执行完下边的代码,释放锁之后才能被其他线程用。
锁的深刻理解
锁定实现方式:
**1.硬件级实现:**短暂关闭时钟中断,对于外部来的中断信号,cpu不去响应。
通过关闭中断让临界区代码「原子执行」,外部中断信号直接被 CPU 硬件忽略,完全不用处理,此操作有风险,谨慎关闭。
2.软件时实现: 对于软件层面的实现,大多数体系结构都提供了swap 或exchange指令,该指令的作用是把寄存器和内存单元数据做交换,由于只有一条指令所以保证了原子性。

所以:我们使用swap,exchange将内存的变量交换到cpu的寄存器中,本质是当前线程/进程,在获取锁,因为整个过程是把数据交换的,不是拷贝,所以该数据只有一份,所以谁申请到这份数据就是谁的,谁就持有锁。
锁的封装
cpp
class Mutex{
public:
Mutex(){
pthread_mutex_init(&_mutex,nullptr);
}
void Lock(){
pthread_mutex_lock(&_mutex);
}
void UnLock(){
pthread_mutex_unlock(&_mutex);
}
~Mutex(){
pthread_mutex_destroy(&_mutex);
}
private:
pthread_mutex_t _mutex;
};
class LockGuard{ //让代码块能够自动调用加/解锁 RAII风格
public:
LockGuard(Mutex &mutex):_mutex(mutex){
_mutex.Lock();//加锁
}
~LockGuard(){
_mutex.UnLock();//解锁
}
private:
Mutex & _mutex;
};
线程同步
线程互斥解决了各执行流访问临界资源时数据不一致问题。
线程同步在线程互斥基础上可以解决线程访问临界资源的顺序性,保证线程访问更加高效。
相关概念
**同步:**在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免
饥饿问题,叫做同步。
**竞态条件:**因程序执行时序(执行顺序、执行时机)的不确定性,导致程序出现异常结果、行为不一致甚至崩溃的问题,就是竞态条件。
条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了,只有其他线程达到某种触发条件当前线程才有可能做出行动。这个触发条件我们称之为条件变量
简单理解:(自我复习理解)
假设有一个线程去放资源,其他线程去获取资源。这个过程对于所有线程来讲去访问临界资源的时候,并不清楚临界资源是否存在。只是申请锁后去访问的时候才会知道是否存在临界资源。这时就会出现一个问题,如果说放资源的线程并不清楚需要资源的线程是否已经拿走这部分资源,放资源的线程就会一直申请锁,导致获取资源的线程长时间无法获得锁去访问临界资源。这就会导致线程饥饿问题。这时只需要线程维护一个条件变量,当放完资源之后,解锁立马去提醒等待资源的线程去获取。这时需要资源的线程申请锁拿到资源,再次触发条件变量,让放资源的线程去放资源。
所以触发条件就是其中能让整个过程高效。我们把这个触发条件称之为环境变量。
条件变量相关接口
条件变量的初始化
全局的:pthread_cond_t cond =PTHREAD_COND_INITIALTZER;
局部的:int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL
条件变量的销毁
int pthread_cond_destroy(pthread_cond_t *cond);
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict
mutex);
参数:
cond:要指定条件变量上等待
mutex:互斥量/锁
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒在指定条件下等待的所有线程。
int pthread_cond_signal(pthread_cond_t *cond);//欢迎在该条件变量下等待的一个进程。
生产者消费者模型
概念
生产者和消费者模型是多线程 / 多进程编程中经典的并发设计模式,核心目的是通过一个中间容器来解决生产者和消费者的强耦合问题。平衡两者的处理速度差异,提高系统整体的并发性能和资源利用率。
模型组成结构
**1.生产者:**负责生成数据,并将数据提交到一个 "中间容器",不直接与消费者交互。
**2.消费者:**负责从 "中间容器" 中获取数据并进行处理,不直接从生产者获取数据。
**3.中间容器:**生产者和消费者之间的 "桥梁",用于缓存生产者生成的数据,解决生产者生产速度与消费者处理速度不匹配的问题,通常是线程安全的队列(FIFO 先进先出为主)。
核心理解:"321"原则
3种关系:
消费者之间的关系:互斥关系
生产者之间的关系:互斥关系
生产者和消费者之间的关系:互斥和同步
2种角色:
生产者和消费者角色
1种交易场所:
特定结构的一种内存
基于blockqueue的生产消费模型

在多线程编程中,阻塞队列是一种常用于实现生产者和消费者模型的结构。与普通队列相比,区别在于当对列为空时,消费者无法从队列中获取数据并且被阻塞,直到生产者放入数据到队列中。当队列满时生产者往队列中放数据也会被阻塞,直到消费者从队列拿出数据,才会解除阻塞。
c++模拟实现blockqueue
其中需要的锁和条件变量模块都是我们模拟实现出来的,分别对应"Mutex.hpp"和"Cond.hpp"模块。
cpp
//"Mutex.hpp"
#pragma once
#include<iostream>
#include<pthread.h>
class Mutex{
public:
pthread_mutex_t* Get(){
return &_mutex;
}
Mutex(){
pthread_mutex_init(&_mutex,nullptr);
}
void Lock(){
pthread_mutex_lock(&_mutex);
}
void UnLock(){
pthread_mutex_unlock(&_mutex);
}
~Mutex(){
pthread_mutex_destroy(&_mutex);
}
private:
pthread_mutex_t _mutex;
};
class LockGuard{
public:
LockGuard(Mutex &mutex):_mutex(mutex){
_mutex.Lock();//加锁
}
~LockGuard(){
_mutex.UnLock();//解锁
}
private:
Mutex & _mutex;
};
cpp
//"Cond.hpp"
#pragma once
#include<iostream>
#include<pthread.h>
#include"Mutex.hpp"
using namespace std;
class Cond{
public:
Cond(){
pthread_cond_init(&_cond,nullptr);
}
void Wait(Mutex &mutex){
pthread_cond_wait(&_cond,mutex.Get());
}
void Signal(){
pthread_cond_signal(&_cond);
}
void Broadcast(){
pthread_cond_broadcast(&_cond);
}
~Cond(){
pthread_cond_destroy(&_cond);
}
private:
pthread_cond_t _cond;
};
cpp
//"BlockQueue.hpp"
#pragma once
#include<iostream>
#include<pthread.h>
#include<queue>
#include<unistd.h>
#include"Mutex.hpp"
#include"Cond.hpp"
using namespace std;
const int Default=5;
template<typename T>
class BlockQueue{
public:
bool full(){
return _q.size()==_cap;
}
bool empty(){
return _q.empty();
}
BlockQueue():_cap(Default),_csleep(0),_psleep(0)
{
// //初始化锁/条件变量
// pthread_mutex_init(&_mutex,nullptr);
// pthread_cond_init(&_full_cond,nullptr);
// pthread_cond_init(&_empty_cond,nullptr);
};
void Equeue(T &data){
//pthread_mutex_lock(&_mutex);
LockGuard lockguard(_mutex);
//生产者生产输入入队列
while(full()){
//满就条件等待
cout<<"数据满了"<<endl;
_psleep++;
//pthread_cond_wait(&_full_cond,&_mutex);
_full_cond.Wait(_mutex);
_psleep--;
}
//入栈
_q.push(data);
if(_csleep){
//pthread_cond_signal(&_empty_cond);
_empty_cond.Signal();
cout<<"唤醒consumer"<<endl;
}
//pthread_mutex_unlock(&_mutex);
}
T Pop(){
//pthread_mutex_lock(&_mutex);
LockGuard lockguard(_mutex);
if(empty()){
cout<<"没数据了"<<endl;
//消费者消费数据,没数据就阻塞
_csleep++;
//pthread_cond_wait(&_empty_cond,&_mutex);
_empty_cond.Wait(_mutex);
_csleep--;
}
//出数据
T data=_q.front();
_q.pop();
if(_psleep){
//pthread_cond_signal(&_full_cond);
_full_cond.Signal();
cout<<"唤醒productor"<<endl;
}
// pthread_mutex_unlock(&_mutex);
return data;
}
~BlockQueue()
{
// pthread_mutex_destroy(&_mutex);
// pthread_cond_destroy(&_full_cond);
// pthread_cond_destroy(&_empty_cond);
};
private:
queue<T> _q;//维护阻塞队列
int _cap;//阻塞队列的大小
Mutex _mutex;//锁
Cond _full_cond;//生产控制条件
Cond _empty_cond;//消费控制条件
int _csleep;
int _psleep;
};
cpp
//"Task.hpp"
#include<iostream>
using namespace std;
class Task{
public:
Task(int x,int y):_x(x),_y(y){}
int result(){
return _x+_y;
}
~Task(){};
private:
int _x;
int _y;
};
cpp
//"main.cc"
#include"BlockQueue.hpp"
#include"Task.hpp"
using namespace std;
void* procuctor(void* args){
//生产者
BlockQueue<Task>* bq=static_cast<BlockQueue<Task>*>(args);
//队列放数据
int x=1;
int y=1;
while(true){
Task t(x,y);
cout<<"prodoctor发布了一个任务:"<<x<<"+"<<y<<"=?"<<endl;
bq->Equeue(t);
sleep(1);
x++;
y++;
}
return nullptr;
}
void* consumer(void* args){
BlockQueue<Task>* bq=static_cast<BlockQueue<Task>*>(args);
//消费者从队列拿数据
while(true){
Task t=bq->Pop();
cout<<"consumer进行处理任务,计算结果:"<<t.result()<<endl;
}
return nullptr;
}
int main(){
//申请阻塞队列
BlockQueue<int>* bq=new BlockQueue<int>();
//构建单个消费者和单个生产者
pthread_t c[4],p[4];
pthread_create(p,nullptr,procuctor,bq);
pthread_create(p+1,nullptr,procuctor,bq);
pthread_create(p+2,nullptr,procuctor,bq);
pthread_create(c,nullptr,consumer,bq);
pthread_create(c+1,nullptr,consumer,bq);
pthread_join(p[0],nullptr);
pthread_join(p[1],nullptr);
pthread_join(p[2],nullptr);
pthread_join(c[0],nullptr);
pthread_join(c[1],nullptr);
return 0;
}
POSIX信号量
POSIX信号量是用于同步操作,达到无冲突访问共享资源,可以用于线程间同步。
信号量相关接口
信号量初始化
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享,默认为0
value:信号量初始值
信号量销毁
int sem_destroy(sem_t *sem);
信号量等待
int sem_wait(sem_t *sem);
//获取信号量,信号量--
信号量发送
int sem_post(sem_t *sem);
//发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1
基于环形队列的生产消费模型
cpp
//模拟实现的信号量原生接口
#pragma once
#include<iostream>
#include<semaphore.h>
using namespace std;
static int Default=5;
class Sem{
public:
Sem(const int sem_value=5)
{
sem_init(&_sem,0,sem_value);
}
void P(){
sem_wait(&_sem);
}
void V(){
sem_post(&_sem);
}
~Sem(){
sem_destroy(&_sem);
}
private:
sem_t _sem;
};
cpp
//基于信号量,环形队列实现的生产者和消费者模型、
#include <iostream>
#include <pthread.h>
#include "Sem.hpp"
#include <vector>
#include <unistd.h>
#include "Mutex.hpp"
using namespace std;
static const int scap = 5;
template <typename T>
class RingQueue
{
public:
RingQueue(int cap = scap)
: _cap(cap), _blank_sem(cap), _data_sem(0), _p_step(0), _c_step(0), _vq(cap)
{
}
void Equeue(T &data)
{
// 生产者生产数据
// 先看看有没有空位置去生产
_blank_sem.P(); // 先申请信号量在申请锁,效率高
{
LockGuard lodckguard(_pmutex);
// 申请位置
_vq[_p_step] = data;
_p_step++; // 成功放入空位置,到下一个位置
_p_step %= _cap; // 保证环形
}
_data_sem.V(); // 有控制位置了
}
void Pop(T *data)
{
// 消费者消费数据
// 先看看有没有数据
_data_sem.P(); // 申请数据
// 有数据
{
LockGuard lodckguard(_cmutex);
*data = _vq[_c_step];
_c_step++;
_c_step %= _cap;
}
_blank_sem.V();
}
~RingQueue()
{
}
private:
vector<T> _vq; // 维护的环形队列
int _cap; // 环形队列的空间
Sem _blank_sem; // 队列中有几个空位置,生产者放数据
int _p_step; // 记录放数据的位置
Sem _data_sem; // 队列中存在数据的位置
int _c_step;
// 两把锁
Mutex _cmutex;
Mutex _pmutex;
};