可重入和线程安全
一些可重入和线程安全的内容:
线程安全:多个线程并发同一段代码时,不会出现不同的结果,常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入
一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数
线程不安全有这样的情况:
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
线程安全有这样的情况:
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
有不可重入的情况:
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
有可重入的情况:
不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入和线程安全的联系:
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
可重入和线程安全的区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
什么啊?我哪里敢说话
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态
死锁的四个必要条件:
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
除了这几个还需要:加锁顺序一致,避免锁未释放的场景,资源一次性分配
条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件
认识接口
这是条件变量的接口,乱七八糟的
创建初始化、销毁、等待、唤醒...
认识条件变量
假设有个人,他名为,鸡婆的棕褐色猎犬(他还要上画面了),那算了,假设有一只狗,它的面前有一个盘子,然后有另一个人,我们在这里简称他为瑶瑶子,瑶瑶子在往猎犬的盘子里放苹果,猎犬要吃盘子里的苹果,那么这个盘子可以被看成共享资源,那瑶瑶子正在往盘子里放苹果,猎犬拿到了还是没拿到?首先先知道瑶瑶子有没有放完(这是不确定的),也就注定了鸡婆的棕褐色猎犬有没吃到苹果还是不确定的,这一人一狗就是线程,一人一狗在进行交互的时候需要保证盘子的安全(pthread_mutex_lock与pthread_mutex_unlock)要保证盘子的安全,访问的时候是互斥的
那么这个时候就会出现一种很尴尬的情况,假设狗申请锁拿到了,进入临界区,那么它先要做的不是吃苹果,而是看临界区有没有苹果,如果没有苹果呢?它只能无能狂怒,解锁后退出,一旦它退出了,它又会急不可耐的去巴巴地守着盘子看有没有苹果(出来不知道盘子里有没苹果,感觉用盒子来举例子更合适。。。但是就先这样看吧我不愿意改了),猎犬只能不断的申请锁,判断,释放锁
那么一个很尴尬的情况出现了,人肯定没有狗快,狗一直吵吵着吃,人根本找不到时机喂它,线程B竞争不过线程A,一直是线程A在进行加锁解锁的操作,这就很不合理啊
于是另一种机制衍生了,准备一个铃铛!(又是什么奇怪的play)
瑶瑶子把苹果放到盘子里就会摇铃召唤猎犬过来吃,每次瑶瑶子喂狗都会先摇铃(你们两个)
这样大家都开心
条件变量的两个特征:
1.需要有一个线程队列
2.需要有通知机制(叫醒一个/全部叫醒)
测试代码
我写的代码也不高级啊,为什么它老是显示服务器崩溃,,,
我是这样解决问题的:
遇到问题--问chat--感觉麻烦--存放问题
能用就行了,去纠结那个干嘛呢
cpp
#include<iostream>
#include<string>
#include<unistd.h>
#include<pthread.h>
const int num = 5;
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER; //条件变量的初始化
void* Wait(void* args)
{
std::string name = static_cast<const char*>(args);
while (true)
{
pthread_mutex_lock(&gmutex);
pthread_cond_wait(&gcond,&gmutex);
std::cout << "I am:" << name << std::endl;
pthread_mutex_unlock(&gmutex);
sleep(1);
}
}
int main()
{
pthread_t threads[num];
for(int i=0;i<num;i++)
{
char *buffer=new char[1024];
snprintf(buffer,1024,"thread-%d",i + 1);
pthread_create(threads+i,nullptr,Wait,(void*)buffer);
}
//唤醒这个线程
while (true)
{
pthread_cond_signal(&gcond);
std::cout << "唤醒一个线程..." << std::endl;
sleep(2);
}
for(int i=0;i<num;i++)
{
pthread_join(threads[i],nullptr);
}
return 0;
}
为什么pthread_cond_wait需要互斥量?
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须 要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有 互斥锁就无法安全的获取和修改共享数据
如果解锁和等待不是原子操作,调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远 阻塞在这个 pthread_cond_wait ,所以解锁和等待必须是一个原子操作
这是唤醒:
进入函数int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 后, 会去看条件量等不等于0,等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样
生产消费模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题
生产者和消费者彼此之间不直接通讯,而 通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的
优点:
解耦
支持并发
支持忙闲不均
假如有一家超市,叫,河马先生,我们买方便面是去这家超市买,超市只负责销售方便面,而不是生产方便面,超市只是供应商,超市是在做缓存,方便面就是数据
切入点:321元哲(原则)
1:一个交易场所(一段内存空间)
2:两种角色(生产角色,消费角色)生产线程、消费线程
3:三种关系(生产和生产,生产和消费,消费和消费)
供应商之间是竞争关系(互斥,同行是冤家)
消费者之间有什么关系(也是互斥,哪个幸运儿会拿走最后一包卡牌呢)
这样可以协调忙闲不均的问题
生产消费要维持同步和互斥
实现生产消费模型本质是通过代码来实现三种关系
有单生产单消费
写个条件变量的代码先
cpp
#pragma once
#include<iostream>
#include<string>
#include<queue>
#include<pthread.h>
const static int defaultcap = 5; //容量上限
template<typename T>
class BlockQueue
{
private:
bool IsFuLL()
{
return _block_queue.size()==_max_cap;
}
bool IsEmpty()
{
return _block_queue.empty();
}
public:
BlockQueue(int cap = defaultcap):_max_cap(cap)
{
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_p_cond,nullptr);
pthread_cond_init(&_c_cond,nullptr);
}
void Pop(T *out)
{
pthread_mutex_lock(&_mutex);
if(IsEmpty())
{
}
pthread_mutex_unlock(&_mutex);
}
void Enqueue(const T &in)
{
pthread_mutex_lock(&_mutex);
if(IsFuLL())
{
//满了生产者不能生产,必须等待
pthread_cond_wait(&_p_cond,&_mutex); //就是我们曾经加的锁
//在临界区里,不这样会导致死锁
//pthread_cond_wait被调用的时候,除了让自己继续排队等待,还会自己释放传入的锁
//函数返回的时候还在临界区
//返回时必须先参与锁的竞争,重新加锁函数才会返回(这样就是在临界区也持有锁了)
}
//没有满/被唤醒了
_block_queue.push(in); //将数据生产到阻塞队列
pthread_mutex_unlock(&_mutex);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_p_cond);
pthread_cond_destroy(&_c_cond);
}
private:
std::queue<T> _block_queue; //临界资源
int _max_cap;
pthread_mutex_t _mutex;
pthread_cond_t _p_cond; //生产者条件变量
pthread_cond_t _c_cond; //消费者条件变量
};
相应的先写出构造函数和析构函数
因为是条件变量的代码,所以要有生产消费者的条件变量
因为容器没有提供判断满的函数,所以我们直接自己封装一下
然后就是写入队出队了,具体逻辑可以看注释,,,
我接着往下写了
生产者生产数据会通知消费者,消费者消费数据会通知生产者
二者在一定程度上协同了:
cpp
#pragma once
#include<iostream>
#include<string>
#include<queue>
#include<pthread.h>
#include<ctime>
const static int defaultcap = 5; //容量上限
template<typename T>
class BlockQueue
{
private:
bool IsFuLL()
{
return _block_queue.size()==_max_cap;
}
bool IsEmpty()
{
return _block_queue.empty();
}
public:
BlockQueue(int cap = defaultcap):_max_cap(cap)
{
srand(time(nullptr));
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_p_cond,nullptr);
pthread_cond_init(&_c_cond,nullptr);
}
void Pop(T *out)
{
pthread_mutex_lock(&_mutex);
while(IsEmpty()) //while可以保证代码的鲁棒性
{
pthread_cond_wait(&_c_cond,&_mutex);
}
//没有空/被唤醒了
*out = _block_queue.front();
_block_queue.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_p_cond); //互相叫醒一下捏
}
void Enqueue(const T &in)
{
pthread_mutex_lock(&_mutex);
if(IsFuLL())
{
//满了生产者不能生产,必须等待
pthread_cond_wait(&_p_cond,&_mutex); //就是我们曾经加的锁
//在临界区里,不这样会导致死锁
//pthread_cond_wait被调用的时候,除了让自己继续排队等待,还会自己释放传入的锁
//函数返回的时候还在临界区
//返回时必须先参与锁的竞争,重新加锁函数才会返回(这样就是在临界区也持有锁了)
}
//没有满/被唤醒了
_block_queue.push(in); //将数据生产到阻塞队列
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_c_cond);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_p_cond);
pthread_cond_destroy(&_c_cond); //互相叫醒一下捏
}
private:
std::queue<T> _block_queue; //临界资源
int _max_cap;
pthread_mutex_t _mutex;
pthread_cond_t _p_cond; //生产者条件变量
pthread_cond_t _c_cond; //消费者条件变量
};
但是真的有必要每次都通知一下么?可不可以设置一种策略到什么时候通知一下这样呢?
可以捏
加个判断就行
比如超过高水平线再执行通知的代码
上面的代码有点错误就是,
cpp
if(IsEmpty())
{
pthread_cond_wait(&_c_cond,&_mutex);
}
这里如果有两个消费者,那会造成,添加尚未满足但是线程被异常唤醒的情况,这种情况叫做伪唤醒,所以应该用while
介素输出:
cpp
#include"BlockQueue.hpp"
#include<pthread.h>
#include<ctime>
#include<unistd.h>
void* Consumer(void* args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);
while (true)
{
sleep(1);
//获取数据
int data = 0;
bq->Pop(&data);
//处理数据
std::cout << "Consumer -> " << data << std::endl;
}
}
void *Productor(void* args)
{
srand(time(nullptr)^getpid());
BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);
while(true)
{
//构建数据
int data = rand()%10+1; //[1,10]
//生产数据
bq->Enqueue(data);
std::cout << "Productor -> " << data << std::endl;
}
}
int main()
{
BlockQueue<int> *bq = new BlockQueue<int>();
pthread_t c,p; //生产者消费者
pthread_create(&c,nullptr,Consumer,bq);
pthread_create(&p,nullptr,Productor,bq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
供应商产生任务也要花时间
生产消费模型是,分为单生产单消费,单生产多消费,多生产单消费,多生产多消费
邮专宿舍模型是,分为四人间上床下桌,六人间上床下桌,和六人间上床下铺
多执行流并发效率很高
不用仅仅局限于消费者和超市
比如供应商在生产任务,其他的供应商也在生产,它放任务和其他供应商生产任务又不冲突(并发)
同理消费者处理任务,其他消费者也在消费
这个是高效
而不是生产消费在超市那里浪费时间
基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别 在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元 素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)