网站建设公司专业公司哪家好,网站的优化用什么软件下载,怎样创建一个平台,公司包装推广⭐️个人主页#xff1a;小羊 ⭐️所属专栏#xff1a;Linux 很荣幸您能阅读我的文章#xff0c;诚请评论指点#xff0c;欢迎欢迎 ~ 目录 1、互斥锁2、生产消费模型2.1 阻塞队列2.2 环形队列 3、单例线程池4、线程安全和重入问题 1、互斥锁 临界资源#xff1a;多线程… ⭐️个人主页小羊 ⭐️所属专栏Linux 很荣幸您能阅读我的文章诚请评论指点欢迎欢迎 ~ 目录 1、互斥锁2、生产消费模型2.1 阻塞队列2.2 环形队列 3、单例线程池4、线程安全和重入问题 1、互斥锁 临界资源多线程执行流共享的资源就叫做临界资源 临界区每个线程内部访问临界资源的代码就叫做临界区 互斥任何时刻互斥保证有且只有一个执行流进入临界区访问临界资源通常对临界资源起保护作用 原子性不会被任何调度机制打断的操作该操作只有两种状态要么完成要么未完成没有中间态
大部分情况线程使用的数据都是局部变量变量的地址空间在线程栈空间内这种情况变量归属单个线程其他线程无法获得这种变量。 但有时候很多变量都需要在线程间共享这样的变量称为共享变量可以通过数据的共享完成线程之间的交互。而多个线程并发的操作共享变量会带来一些问题。 内存中的数据是共享的但是当线程把数据从内存读到CPU中的寄存器中变成线程的上下文数据就变成了私有的而ticketnum--后需要把数据再次写入到内存中如果线程在写入内存前被切换多个线程都执行这一操作就可能会出现ticketnum减为负数的情况。
避免类似上述问题需要解决三个问题
代码必须要有互斥行为当代码进入临界区时不允许其他线程进入该临界区。如果多个线程同时要求执行临界区的代码并且临界区没有线程在执行那么只能允许一个线程进入该临界区。如果线程不在临界区中执行那么该线程不能阻止其他线程进入临界区。
要做到这三点本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
| 线程或进程什么时候被切换
时间片耗尽时有更高优先级的进程要调度时通过sleep从内核返回用户时会进行时间片是否到达的检测进而导致切换 如果锁对象是全局的或静态的可以用宏PTHREAD_MUTEX_INITIALIZER初始化并且不用我们主动destroy如果锁对象是局部的需要用pthread_mutex_init初始化用pthread_mutex_destroy释放。 所有对资源的保护都是对临界区代码的访问因为资源都是通过代码访问的。要保证加锁的细粒度。加锁就是找到临界区对临界区进行加锁。
那么相应的又有一些问题
锁也是全局的共享资源谁保证锁的安全加锁和解锁被设计为原子的。如果看待锁加锁本质就是对资源的预定工作整体使用资源所以加锁前先要申请锁。如果申请锁的时候锁已经被别的线程拿走了怎么办其他线程阻塞等待。线程在访问临界区的时候可不可以被切换可以我被切走其他线程也不能进来因为我走的时候是带着锁走的保证了原子性。 lock是原子的其他线程无法进入锁是如何实现的 为了实现互斥锁操作大多数体系结构都提供了swap或exchange指令该指令的作用是把寄存器和内存单元的数据交换私有和共享由于只有一条指令保证了原子性即使是多处理器平台访问内存的总线周期也有先后一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 #pragma once#include iostream
#include pthread.hnamespace MutexModule
{class Mutex{public:Mutex(const Mutex) delete;const Mutex operator(const Mutex) delete;Mutex(){int n pthread_mutex_init(_lock, nullptr);}void Lock(){int n pthread_mutex_lock(_lock);}void Unlock(){int n pthread_mutex_unlock(_lock);}pthread_mutex_t* LockPtr(){return _lock;}~Mutex(){int n pthread_mutex_destroy(_lock);}private:pthread_mutex_t _lock;};class LockGuard{public:LockGuard(Mutex mutex):_mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex _mutex;};
}2、生产消费模型
2.1 阻塞队列
当一个线程互斥地访问某个变量时它可能发现在其他线程改变状态之前它什么都做不了。例如一个线程访问队列时发现队列为空它只能等待直到其他线程将一个节点添加到队列中这种情况就需要条件变量。
生产者消费者模型
生产者和生产者互斥消费者和消费者互斥生产者和消费者互斥 同步
总结3种关系2种角色1个交易区。 pthread_cond_wait是一个函数只要是函数就可能会出错如果这个函数调用出错继续往下执行代码但是当前阻塞队列中已经满了再向其中放数据就出错了。还有如果阻塞等待的线程和其他线程所发的信号数量不匹配也就是出现了伪唤醒也会出现问题。 为了规避这种问题这里判断阻塞队列是否为满我们就不同if判断了而是改用while判断只要阻塞队列为空就一直阻塞等待这样做可以规避很多可能出现的错误。
》上面实现的生产者消费者模型只是单生产单消费只保证了生产者和消费者之间的互斥关系如果增加线程让其变成多生产多消费该如何修改 事实上我们只需要增加对应的线程即可因为我们的临界区只用了一把互斥锁保护生产者和生产者之间消费者和消费者之间也可以保证互斥的关系。
》这里我们实现的生产者消费者模型在访问临界区资源时是互斥的不难发现它运行的效率不是很高那我们该怎么保证生产者消费者模型的效率问题呢 事实上我们看待生产者消费者模型不能只聚焦于临界区这小点来思考为什么说是一小点呢生产者要生产数据实际上是往临界区中放数据也是需要从外部获取“原料”的消费者消费数据实际上是从临界区中取数据拿到后也需要处理数据不管是生产者从别的地方获取“原料”还是消费者处理数据都是需要时间的。也就是说在生产者从外部获取资源的时候消费者可以随意访问临界区在消费者处理数据的时候生产者也可以随意访问临界区这个时候生产者和消费者是并行的也就是说只有生产者访问临界区相对于消费者访问临界区是串行的这是生产者消费者模型高效率的关键。
#pragma once
#include iostream
#include pthread.h
#include queue
#include unistd.h
#include Mutex.hpp
#include Cond.hppnamespace BlockQueueModule
{// version 2using namespace CondModule;using namespace MutexModule;templateclass Tclass BlockQueue{private:bool IsFull() {return _cap _q.size();}bool IsEmpty() {return _q.empty();}public:BlockQueue(int cap 10):_cap(cap), _cwait(0), _pwait(0){}void Equeue(const T data) //生产者{LockGuard lockguard(_mutex);//RAIIwhile (IsFull()){std::cout 生产者进入等待... std::endl;_pwait;//如果阻塞队列为满则等待消费者发信号//wait首先解锁让其他线程有访问临界区的机会_producter_signal.Wait(_mutex);_pwait--;std::cout 生产者被唤醒... std::endl;//wait被唤醒后重新申请锁访问临界区}//走到这里说明生产者收到了消费者发的信号临界区一定有空位置可以生产了_q.push(data);//生产者放完数据一定有数据给消费者发信号可以消费了if (_cwait){_consumer_signal.Notify();}}void Pop(T *data) //消费者{LockGuard lockguard(_mutex);//RAIIwhile (IsEmpty()){std::cout 消费者进入等待... std::endl;_cwait;//如果阻塞队列为空则等待生产者发信号_consumer_signal.Wait(_mutex);_cwait--;std::cout 消费者被唤醒... std::endl;}//走到这里说明消费者收到了生产者发的信号临界区一定有数据可以消费了*data _q.front();_q.pop();//消费者取完数据一定有空位置给生产者发信号可以生成了if (_pwait){_producter_signal.Notify();}}~BlockQueue(){}private:std::queueT _q; //临界资源int _cap; //默认临界区大小Mutex _mutex; //互斥锁Cond _producter_signal; //生产者条件变量Cond _consumer_signal; //消费者条件变量int _cwait; //有多少个消费者在阻塞等待int _pwait; //有多少个生产者在阻塞等待};// version 1// static const int gcap 10; //默认临界区大小// templateclass T// class BlockQueue// {// private:// bool IsFull()// {// return _cap _q.size();// }// bool IsEmpty()// {// return _q.empty();// }// public:// BlockQueue(int cap gcap)// :_cap(cap), _cwait(0), _pwait(0)// {// pthread_mutex_init(_mutex, nullptr);// pthread_cond_init(_producter_signal, nullptr);// pthread_cond_init(_consumer_signal, nullptr);// }// void Equeue(const T data) //生产者// {// pthread_mutex_lock(_mutex);// while (IsFull())// {// std::cout 生产者进入等待... std::endl;// _pwait;// //如果阻塞队列为满则等待消费者发信号// //wait首先解锁让其他线程有访问临界区的机会// pthread_cond_wait(_producter_signal, _mutex);// _pwait--;// std::cout 生产者被唤醒... std::endl;// //wait被唤醒后重新申请锁访问临界区// }// //走到这里说明生产者收到了消费者发的信号临界区一定有空位置可以生产了// _q.push(data);// //生产者放完数据一定有数据给消费者发信号可以消费了// if (_cwait)// {// pthread_cond_signal(_consumer_signal);// }// pthread_mutex_unlock(_mutex);// }// void Pop(T *data) //消费者// {// pthread_mutex_lock(_mutex);// while (IsEmpty())// {// std::cout 消费者进入等待... std::endl;// _cwait;// //如果阻塞队列为空则等待生产者发信号// pthread_cond_wait(_consumer_signal, _mutex);// _cwait--;// std::cout 消费者被唤醒... std::endl;// }// //走到这里说明消费者收到了生产者发的信号临界区一定有数据可以消费了// *data _q.front();// _q.pop();// //消费者取完数据一定有空位置给生产者发信号可以生成了// if (_pwait)// {// pthread_cond_signal(_producter_signal);// }// pthread_mutex_unlock(_mutex);// }// ~BlockQueue()// {// pthread_mutex_destroy(_mutex);// pthread_cond_destroy(_producter_signal);// pthread_cond_destroy(_consumer_signal);// }// private:// std::queueT _q; //临界资源// int _cap; //最大容量// pthread_mutex_t _mutex; //互斥锁// pthread_cond_t _producter_signal; //生产者条件变量// pthread_cond_t _consumer_signal; //消费者条件变量// int _cwait; //有多少个消费者在阻塞等待// int _pwait; //有多少个生产者在阻塞等待// };
}2.2 环形队列
对于环形队列重要的一点是为空或为满指针指向的是同一个位置。如果为空保证生产者先原子性的生产如果为满保证消费者原子性的先消费。这里体现出互斥是通过信号量来实现的。 资源用信号量表示任何人访问临界资源之前都必须先申请信号量信号量表示资源的数目。资源分为空间资源和数据资源对于生产者来说他关注的是空间资源对于消费者来说他关注的是数据资源。
》前面的阻塞队列中不管是生产还是消费前都要先判断为什么环形队列这里没有判断呢 因为这里信号量本身就是表示资源数目只要成功就一定有不需要判断。
上面已经基本实现了生产者和消费者之间的同步和互斥关系那么多生产多消费中生产者和生产者消费者和消费者之间的互斥关系如何保证呢
事实上上面生产者和消费者之间的同步和互斥关系是通过信号量来保证的也就是说单生产和单消费这里不需要互斥锁在这里互斥锁我们只需要用来处理生产者和生产者消费者和消费者之间的互斥关系就行。而我们知道环形队列中读写位置各自只有一个所以多线程之间的生产和消费最后还是单生产单消费问题所以我们只需要用两把锁一把锁守护生产权利一把锁守护消费权利让多线程先竞争这把锁然后生产或消费。 》如上我们应该在哪个位置加锁更好一点呢 首先不管在哪个位置加锁我们都能保证生产者之间消费者之间都是互斥的关系。其次多个线程申请锁不管最后谁是赢家可以肯定的是赢家只有一个也就是说如果先让多线程申请锁然后再有这个赢家申请信号量而如果先让多个线程申请信号量信号量可以是多个所以最后赢家可能有多个然后再让这些个线程去竞争锁在这些线程竞争锁的同时其他没申请到信号量的线程如果有信号量了也可以同时申请信号量也就是可以达到并行。 这就好比看电影如果在前面加锁就像是我们先排队然后在买票一次只能进去一个同学如果在后面加锁就像是我们先买票然后在排队进入在买到票的同学排队进入放映厅的过程中后面来的同学都可以先去买票很明显第二种效率更高。
#pragma once#include iostream
#include vector
#include unistd.h
#include pthread.h
#include Mutex.hpp
#include Cond.hpp
#include Sem.hppnamespace RingQueueModule
{using namespace MutexModule;using namespace CondModule;using namespace SemModule;templateclass Tclass RingQueue{public:RingQueue(int cap):_ring(cap), _cap(cap), _p_step(0),_c_step(0), _spacesem(cap), _datasem(0){}void Equeue(const T data){// 1.LockGuard lockguard(_p_lock);//先申请信号量在竞争锁效率更高_spacesem.P();LockGuard lockguard(_p_lock);_ring[_p_step] data;_p_step % _cap;_datasem.V();}void Pop(T *out){_datasem.P();LockGuard lockguard(_c_lock);*out _ring[_c_step];_c_step % _cap;_spacesem.V(); //释放资源信号量1}~RingQueue(){}private:std::vectorT _ring; //临界资源int _cap; //临界空间大小int _p_step; //生产者位置int _c_step; //消费者位置Sem _spacesem; //空间信号量Sem _datasem; //数据信号量Mutex _p_lock; Mutex _c_lock;};
}3、单例线程池 #pragma once#include iostream
#include vector
#include queue
#include memory
#include Thread.hpp
#include Mutex.hpp
#include Cond.hpp
#include Log.hpp// 懒汉模式线程池
namespace ThreadPoolModule
{using namespace ThreadModule;using namespace LogModule;using namespace CondModule;using namespace LogModule;using thread_t std::shared_ptrThread;const static int threadnum 5;template class Tclass ThreadPool{private:bool IsEmpty(){return _taskq.empty();}// 处理任务void HandlerTask(std::string name){LOG(LogLevel::DEBUG) 线程: name , 进入HandlerTask...;while (true){T t;{LockGuard lockguard(_lock); // 在任务队列中拿任务需要加锁保护while (IsEmpty() _isrunning) // 只有任务队列为空 线程池在运行线程才需要等待休眠{_wait_num;_cond.Wait(_lock);_wait_num--;}if (IsEmpty() !_isrunning) // 只有任务队列为空 线程池退出所有的线程才不需要等待break;t _taskq.front();_taskq.pop();}t(name); // 线程有独立栈处理任务不需要被保护}LOG(LogLevel::INFO) 线程 name 退出...;}ThreadPool(int num threadnum): _num(num), _wait_num(0), _isrunning(false){for (int i 0; i _num; i){// 非静态成员函数取函数指针需要加_threads.push_back(std::make_sharedThread(std::bind(ThreadPool::HandlerTask, this, std::placeholders::_1)));LOG(LogLevel::INFO) 创建线程 _threads.back()-Name();}}public:// 不支持拷贝、赋值ThreadPool(const ThreadPoolT) delete;const ThreadPoolT operator(const ThreadPoolT) delete;// 1.将构造函数私有化让其不能在类外实例化对象只能在类里面创建一个对象// 2.创建单例对象的函数设为静态函数在类外通过指定类域的方式访问这个静态成员函数获取单例对象// 3.因为构造函数被私有类外不能创建对象非静态成员函数只能通过对象访问不能通过指定类域的方式访问static ThreadPoolT *GetInstance(){if (_instance nullptr){LockGuard lockguard(_mtx);if (_instance nullptr){LOG(LogLevel::INFO) 单例首次被执行需要加载对象...;_instance new ThreadPoolT();}}return _instance;}void Equeue(const T in){LockGuard lockguard(_lock);if (_isrunning){_taskq.push(in);if (_wait_num 0){_cond.Notify();}}}void Start(){LockGuard lockguard(_lock);if (_isrunning)return;_isrunning true;for (auto threadptr : _threads){threadptr-Start();LOG(LogLevel::INFO) 启动线程 threadptr-Name();}}void Wait(){for (auto threadptr : _threads){threadptr-Join();LOG(LogLevel::INFO) 回收线程 threadptr-Name();}}void Stop(){LockGuard lockguard(_lock);if (_isrunning){// 退出线程池必须保证// 1.不能再进任务_isrunning false;// 2.线程池内的任务必须全部处理完// 3.让线程自己退出被唤醒if (_wait_num 0){_cond.NotifyAll(); // 将等待中的线程全部唤醒如果有任务处理完剩余任务线程正常回收}}}~ThreadPool(){}private:std::vectorthread_t _threads; // 管理线程std::queueT _taskq; // 管理任务int _num; // 线程个数int _wait_num; // 正在等待任务的线程个数Mutex _lock;Cond _cond; // 等待任务bool _isrunning; // 线程池状态static ThreadPoolT* _instance; // 单例对象static Mutex _mtx;};templateclass TThreadPoolT* ThreadPoolT::_instance nullptr;templateclass TMutex ThreadPoolT::_mtx;
} 4、线程安全和重入问题
线程安全就是多个线程在访问共享资源时能够正确地执行不会相互干扰或破坏彼此的执行结果。一般而言多个线程并发同一段只有局部变量的代码时不会出现不同的结果。但是对全局变量或者静态变量进行操作并且没有锁保护的情况下容易出现该问题。重入同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则是不可重入函数。
重入可以分为两种情况
多线程重入函数信号导致一个执行流重复进入函数
| 可重入和线程安全的联系
函数是可重入的那就是线程安全的。函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题。如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。
| 可重入与线程安全区别
可重入函数是线程安全函数的一种。线程安全不一定是可重入的而可重入函数则一定是线程安全的。如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数有锁还未释放则会产生死锁因此是不可重入的。 本篇文章的分享就到这里了如果您觉得在本文有所收获还请留下您的三连支持哦~