线程池+单例模式+STL,智能指针和线程安全+其他常见的各种锁+读者写者问题
- 1.线程池
- 2.线程安全的单例模式
- 3.STL,智能指针和线程安全
- 4.其他常见的各种锁
- 4.读者写者问题
喜欢的点赞,收藏,关注一下把!
1.线程池
目前我们学了挂起等待锁、条件变量、信号量、生产者消费者模型那我们就根据这些写一个线程池!
- 线程池:
- 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
- 线程池的应用场景:
-
- 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技
术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个
Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
- 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技
-
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
-
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情
况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,
出现错误.
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情
- 线程池的种类:
- 线程池示例:
-
- 创建固定数量线程池,循环从任务队列中获取任务对象,
-
- 获取到任务对象后,执行任务对象中的任务接口
一般我们都是来了一个任务我们才创建一个线程,这种模式在处理任务虽然没有问题,但是这样做可能会导致效率降低,因为创建线程也是有成本的。
就像我们在学习STL的时候,看到vector扩容是按1.5倍扩容的,本来我只需要10个字节,可能它会给你15个字节,多出来的空间当你后序还想扩容可能者这多出来空间就够了不需要再扩容了,因为扩容也是要花费时间的。这种思想就是池化技术。
未来我们先提前创建出一批线程,让这些线程去扫描任务队列中有没有任务,有任务让这些线程去拿任务然后处理任务。没任务就全部休眠而不推出!唤醒一个线程的成本可比创建一个线程的成本要低的多 。
因此我们的模型如下:
这就是我们的线程池!
当看到这个图时,如果把以前的知识学的扎实,这份代码其实自己就会写了。因为它就是典型的生产者消费者模式!
接下来我们把线程池写出来。
线程池线程池,首先我们需要有线程。首先把创建线程写出来!
#pragma once
#include<iostream>
#include<string>
#include<pthread.h>
#include<functional>class Thread
{typedef std::function<void*(void*)> func_t;
private://类内成员有隐藏的this指针,不加static就会报错!//但是我们又需要this指针,调用类的成员变量,因此把this传过来static void* start_routine(void* args){Thread* _this=static_cast<Thread*>(args);//安全进行类型转换return _this->_func(_this->_args);//调用回调函数,不这样写也可以再写一个类内函数在调用}
public:Thread(){char namebuffer[64];snprintf(namebuffer,sizeof namebuffer,"thread-%d",_number++);_name=namebuffer;}//为什么这里参数不放在构造函数//因为我们等会想线程运行的时候,知道是那个线程在运行把_name也一起传过去void start(func_t func,void* args){_func=func;_args=args;//这个函数不认识C++的function类,因此我自己写一个函数pthread_create(&_tid,nullptr,start_routine,this);}void join(){pthread_join(_tid,nullptr);}//线程名std::string threadname(){return _name;}~Thread(){}private:std::string _name;//线程名func_t _func;//回调函数void* _args;//回调函数参数pthread_t _tid;//线程IDstatic int _number;
};int Thread::_number=1;
在把放的任务写一下
#pragma once
#include<iostream>
#include<functional>
#include<string>class Task
{typedef std::function<int(int,int,char)> func_t;
public:Task(){};Task(int x,int y,char op,func_t func):_x(x),_y(y),_op(op),_func(func){}std::string operator()(){int result=_func(_x,_y,_op);char buffer[64];snprintf(buffer,sizeof buffer,"%d %c %d = %d",_x,_op,_y,result);return buffer;}std::string toTaskString(){char buffer[64];snprintf(buffer,sizeof buffer,"%d %c %d = ?",_x,_op,_y);return buffer;}private:int _x;int _y;char _op;func_t _func;
};int mymath(int x, int y, char op)
{int result = 0;switch (op){case '+':result=x+y;break;case '-':result=x-y;break;case '*':result=x*y;break;case '/':{if(y == 0){std::cout<<"div zero error"<<std::endl;result=-1;}elseresult=x/y;}break;case '%':if(y == 0){std::cout<<"mod zero error"<<std::endl;result=-1;}elseresult=x%y;break;default:break;}return result;
}
在想线程池的代码之前我们要先想好,需要创建几个线程,这些线程怎么管理,任务放在哪。
因此我们需要一个创建几个线程的变量,创建好的线程我们打算用vector进行管理,任务我们打算放在队列中。
还有一件事情,当线程在队列中拿的时候不能放任务,放任务的时候线程不能拿,并且我们还想让线程有序的进行拿任务。
因此我们还需要一把锁,一个条件变量。
#pragma once
#include"Thread.hpp"
#include"Task.hpp"
#include<vector>
#include<queue>using namespace std;
const int maxcap=3;//声明
template<class T>
class ThreadPool;template<class T>
class ThreadData
{
public:ThreadData(ThreadPool<T>* poolthis,const string& name):_poolthis(poolthis),_name_(name){}~ThreadData(){}
public:ThreadPool<T>* _poolthis;string _name_;
};template<class T>
class ThreadPool
{
private://线程调用的处理任务函数static void* handTask(void* args){ThreadData<T>* td=static_cast<ThreadData<T>*>(args);while(true){//这里我们写了一些函数调用,也可以每个都加this指针调用//放任务之前加锁td->_poolthis->threadlock();while(td->_poolthis->IsQueueEmpty())//任务队列空线程就等待{td->_poolthis->threadwait();}//取任务Task t;td->_poolthis->pop(&t);//注意一定要先解锁,在处理任务!不然串行处理任务一点意义都没有!!td->_poolthis->threadunlock();//线程并行处理任务cout<<td->_name_<<" 处理完了任务: "<<t()<<endl;}}
private:void threadlock(){pthread_mutex_lock(&_lock);}void threadunlock(){pthread_mutex_unlock(&_lock);}void threadwait(){pthread_cond_wait(&_cond,&_lock);}void pop(T* out){*out=_task_queue.front();_task_queue.pop();}bool IsQueueEmpty(){return _task_queue.empty();}public:ThreadPool(int cap=maxcap):_cap(maxcap){//初始化锁,条件变量pthread_mutex_init(&_lock,nullptr);pthread_cond_init(&_cond,nullptr);//创建线程for(int i=0;i<_cap;++i){_threads.push_back(new Thread());//创建线程并放在vector里}}//启动线程//在Thread里说过,想把线程名也传过去,但是回调函数只有一个函数//而这函数我们写在类里面必须要加一个static,导致没有this指针,而使用类内成员需要this指针//因此我们写个类把线程名和this都传过去void run(){for(auto& thread:_threads){ThreadData<T>* td=new ThreadData<T>(this,thread->threadname());thread->start(handTask,td);}}//任务队列放任务void push(const T& in){//保证放任务是安全的,所以先加锁pthread_mutex_lock(&_lock);_task_queue.push(in);pthread_cond_signal(&_cond);//队列中有任务就唤醒等待的线程去取任务pthread_mutex_unlock(&_lock);}~ThreadPool(){//销毁锁,条件变量pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}private:int _cap;//线程个数vector<Thread*> _threads;//线程放在vector里进行管理queue<T> _task_queue;//任务队列pthread_mutex_t _lock;pthread_cond_t _cond;
};
看运行结果,线程池内的线程是有序的在处理任务,符合我们的要求。
前面我们锁的封装我们也不写过好,这里我们也用一用
#pragma once
#include<iostream>
#include<pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t* lock):_lock(lock){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;
};class LockGuard
{
public:LockGuard(pthread_mutex_t* lock):_mutex(lock){_mutex.lock();}~LockGuard(){_mutex.unlock();}
private:Mutex _mutex;
};
在修改一下线程池有关处理任务的代码
//线程调用的处理任务函数static void* handTask(void* args){ThreadData<T>* td=static_cast<ThreadData<T>*>(args);Task t;while(true){//RAII 风格加锁{//构造时自动加锁,析构时自动结束//局部变量生命周期这个代码块LockGuard lockguard(td->_poolthis->mutex());while (td->_poolthis->IsQueueEmpty()){td->_poolthis->threadwait();}td->_poolthis->pop(&t);}cout << td->_name_ << " 处理完了任务: " << t() << endl;}}
private:void threadlock(){pthread_mutex_lock(&_lock);}void threadunlock(){pthread_mutex_unlock(&_lock);}void threadwait(){pthread_cond_wait(&_cond,&_lock);}void pop(T* out){*out=_task_queue.front();_task_queue.pop();}bool IsQueueEmpty(){return _task_queue.empty();}pthread_mutex_t* mutex(){return &_lock;}
这个代码需要大家自己写一写体会一下,才能加深知识的理解,这里没有新的内容都是以往我们学的知识!
2.线程安全的单例模式
什么是单例模式
单例模式是一种 “经典的, 常用的, 常考的” 设计模式.
什么是设计模式
IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是 设计模式
单例模式的特点
某些类, 只应该具有一个对象(实例), 就称之为单例.
例如
一个男人只能有一个媳妇.
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.
饿汉实现方式和懒汉实现方式
【洗碗的例子】
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.
就如在C++定义一些全局变量或静态变量,这些变量在全局数据区提前给我们定义好了。当我们的可执行程序加载到内存的时候,在程序还没有运行之前,我们都要先把当前的对象立马创建出来,就是我还没用就先把它创建出来了,像这种模式就是饿汉模式,当我用的时候不用创建直接拿来就用!
使用之前创建好好了—>饿汉模式
当创建的时候先不创建,在用的时在new或malloc创建 —>懒汉模式
懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度
问:
在我们进行new或malloc申请堆空间时,OS是不是在你申请的时侯就把空间给你了?
答:
肯定不是!你申请和你未来使用肯定还有很长时间,作为OS来说把空间给你了,但是你不会立即使用,你不立即使用这段给你的空间不就处于闲置状态了嘛,对于OS来说为什么要做这个事情呢?
所以你在new或malloc的时候申请的堆空间就没有在物理内存给你开辟,而只是在虚拟地址空间上把你的地址扩大一点然后把起始虚拟地址返回,未来在你真正想对这块空间给你写入时,那么OS底层才会触发缺页中断,然后OS才会在物理内存重新开辟空间,然后重新构建你曾经申请的虚拟地址和物理内存之间的映射关系!
所谓缺页中断就是一旦访问内存时发现虚拟地址到页表之间转化到物理内存没有对应的映射关系,那么此时OS就把你的工作停下来,开始执行OS的代码,就相当于在内存中给你把空间找到,找到之后修改你的页表,重新构建映射关系就好了。
我们平时调用new和malloc在OS层面上,OS也给我们叫做延时开辟。
饿汉方式实现单例模式
template <typename T>
class Singleton {static T data;//直接定义一个单例对象
public:static T* GetInstance() {return &data;//未来获取的时直接获取}
};
饿汉在设置的时候,只需要将类的拷贝构造,赋值等等能构造出对象的这些语句全都delete掉,然后在类中用当前构造函数中定义出静态对象就可以了。静态方法和全局变量在代码加载到内存,在语言的角度在加载到内存的时候对象就已经创建出来跟你定义全局变量一样,在系统的角度定义的全局变量不就在地址空间中的全局数据区吗,所以这个对象在它加载的时候这个对象就已经有了!
相当于只要是全局的或者静态的在进程加载到内存时这个单例就已经有了。
加载时立马就创建这就是饿汉,如果单例很大启动时间就很久。
懒汉方式实现单例模式
template <typename T>
class Singleton {static T* inst;
public:static T* GetInstance() {if (inst == NULL) {inst = new T();}return inst;}
};
定义一个指针,先不创建你的对象,当在用的时候在创建出来。
上面存在一个严重的问题, 线程不安全.
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.
但是后续再次调用, 就没有问题了
懒汉方式实现单例模式(线程安全版本)
// 懒汉模式, 线程安全
template <typename T>
class Singleton {volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.static std::mutex lock;
public:static T* GetInstance() {if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.if (inst == NULL) {inst = new T();}lock.unlock();}return inst;}
};
注意事项:
- 加锁解锁的位置
- 双重 if 判定, 避免不必要的锁竞争
- volatile关键字防止过度优化
下面将我们的线程池改成懒汉模式的单例
#pragma once
#include"Thread.hpp"
#include"Task.hpp"
#include<vector>
#include<queue>
#include"Mutex.hpp"using namespace std;
const int maxcap=3;//声明
template<class T>
class ThreadPool;template<class T>
class ThreadData
{
public:ThreadData(ThreadPool<T>* poolthis,const string& name):_poolthis(poolthis),_name_(name){}~ThreadData(){}
public:ThreadPool<T>* _poolthis;string _name_;
};template<class T>
class ThreadPool
{
private://线程调用的处理任务函数static void* handTask(void* args){ThreadData<T>* td=static_cast<ThreadData<T>*>(args);Task t;while(true){//RAII 风格加锁{//构造时自动加锁,析构时自动结束//局部变量生命周期这个代码块LockGuard lockguard(td->_poolthis->mutex());while (td->_poolthis->IsQueueEmpty()){td->_poolthis->threadwait();}td->_poolthis->pop(&t);}cout << td->_name_ << " 处理完了任务: " << t() << endl}}private:void threadlock(){pthread_mutex_lock(&_lock);}void threadunlock(){pthread_mutex_unlock(&_lock);}void threadwait(){pthread_cond_wait(&_cond,&_lock);}void pop(T* out){*out=_task_queue.front();_task_queue.pop();}bool IsQueueEmpty(){return _task_queue.empty();}pthread_mutex_t* mutex(){return &_lock;}//单例不是没有例,构造函数不能去掉,放在private就好了ThreadPool(int cap=maxcap):_cap(maxcap){//初始化锁,条件变量pthread_mutex_init(&_lock,nullptr);pthread_cond_init(&_cond,nullptr);//创建线程for(int i=0;i<_cap;++i){_threads.push_back(new Thread());//创建线程并放在vector里}}//去掉赋值,拷贝构造void operator=(const ThreadPool&) = delete;ThreadPool(const ThreadPool&) = delete;public://启动线程//在Thread里说过,想把线程名也传过去,但是回调函数只有一个函数//而这函数我们写在类里面必须要加一个static,导致没有this指针,而使用类内成员需要this指针//因此我们写个类把线程名和this都传过去void run(){for(auto& thread:_threads){ThreadData<T>* td=new ThreadData<T>(this,thread->threadname());thread->start(handTask,td);}}//任务队列放任务void push(const T& in){//保证放任务是安全的,所以先加锁pthread_mutex_lock(&_lock);_task_queue.push(in);pthread_cond_signal(&_cond);//队列中有任务就唤醒等待的线程去取任务pthread_mutex_unlock(&_lock);}~ThreadPool(){//销毁锁,条件变量pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}//获取单例//成员函数可以调用静态成员和静态成员函数,反之不行ThreadPool<T>* getInstance(){//线程不安全if(tp == nullptr){tp=new ThreadPool<T>();}return tp;}private:int _cap;//线程个数vector<Thread*> _threads;//线程放在vector里进行管理queue<T> _task_queue;//任务队列pthread_mutex_t _lock;pthread_cond_t _cond;static ThreadPool<T>* tp;
};template<class T>
ThreadPool<T>* ThreadPool<T>::tp=nullptr;
为什么会报错?
原因是因为虽然类内成员函数能够访问静态成员变量,但是getInstance是一个成员函数里面有this指针。
这个成员函数既属性这个类又属于对象,未来我们只想获取的单例只属于类,因此我们需要给这个成员函数前面加个static,那就只属于类了。
首次获取不存在就new一个,否则直接返回
#include"ThreadPool.hpp"
#include<unistd.h>int main()
{//一大堆代码...//ThreadPool<Task>* tp=new ThreadPool<Task>();//tp->run();//获取单例直接run起来ThreadPool<Task>::getInstance()->run();//用的时候才创建int x,y;char op;while(true){cout<<"请输入第一个数据#";cin>>x;cout<<"请输入第二个数据#";cin>>y;cout<<"请输入要进行的操作#";cin>>op;Task t(x,y,op,mymath);ThreadPool<Task>::getInstance()->push(t);sleep(1);}return 0;
}
这就是懒汉模式的单例。
但是这种方式是线程不安全的!tp本身就是一份公共资源!如果两个线程同时调用, 可能会创建出两份 ,T 对象的实例。因此我们需要加锁。
单例完整代码
#pragma once
#include "Thread.hpp"
#include "Task.hpp"
#include <vector>
#include <queue>
#include "Mutex.hpp"
#include <mutex>using namespace std;
const int maxcap = 3;// 声明
template <class T>
class ThreadPool;template <class T>
class ThreadData
{
public:ThreadData(ThreadPool<T> *poolthis, const string &name) : _poolthis(poolthis), _name_(name){}~ThreadData(){}public:ThreadPool<T> *_poolthis;string _name_;
};template <class T>
class ThreadPool
{
private:// 线程调用的处理任务函数static void *handTask(void *args){ThreadData<T> *td = static_cast<ThreadData<T> *>(args);Task t;while (true){// RAII 风格加锁{// 构造时自动加锁,析构时自动结束// 局部变量生命周期这个代码块LockGuard lockguard(td->_poolthis->mutex());while (td->_poolthis->IsQueueEmpty()){td->_poolthis->threadwait();}td->_poolthis->pop(&t);}cout << td->_name_ << " 处理完了任务: " << t() << endl;}}private:void threadlock() { pthread_mutex_lock(&_lock); }void threadunlock() { pthread_mutex_unlock(&_lock); }void threadwait() { pthread_cond_wait(&_cond, &_lock); }void pop(T *out){*out = _task_queue.front();_task_queue.pop();}bool IsQueueEmpty() { return _task_queue.empty(); }pthread_mutex_t *mutex(){return &_lock;}// 单例不是没有例,构造函数不能去掉,放在private就好了ThreadPool(int cap = maxcap) : _cap(maxcap){// 初始化锁,条件变量pthread_mutex_init(&_lock, nullptr);pthread_cond_init(&_cond, nullptr);// 创建线程for (int i = 0; i < _cap; ++i){_threads.push_back(new Thread()); // 创建线程并放在vector里}}// 去掉赋值,拷贝构造void operator=(const ThreadPool &) = delete;ThreadPool(const ThreadPool &) = delete;public:// 启动线程// 在Thread里说过,想把线程名也传过去,但是回调函数只有一个函数// 而这函数我们写在类里面必须要加一个static,导致没有this指针,而使用类内成员需要this指针// 因此我们写个类把线程名和this都传过去void run(){for (auto &thread : _threads){ThreadData<T> *td = new ThreadData<T>(this, thread->threadname());thread->start(handTask, td);}}// 任务队列放任务void push(const T &in){// 保证放任务是安全的,所以先加锁pthread_mutex_lock(&_lock);_task_queue.push(in);pthread_cond_signal(&_cond); // 队列中有任务就唤醒等待的线程去取任务pthread_mutex_unlock(&_lock);}~ThreadPool(){// 销毁锁,条件变量pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}// 获取单例// 成员函数可以调用静态成员和静态成员函数,反之不行static ThreadPool<T> *getInstance(){// 虽然没有并发问题了,但是还有一个小问题// 未来每一个线程进来都要lock,unlock//因此在外面再加一个if判断,未来只要第一次实例化之后就不需要再加锁解锁了//大家就可以并发了if (tp == nullptr){_singlock.lock();if (tp == nullptr){tp = new ThreadPool<T>();}_singlock.unlock();}return tp;}private:int _cap; // 线程个数vector<Thread *> _threads; // 线程放在vector里进行管理queue<T> _task_queue; // 任务队列pthread_mutex_t _lock;pthread_cond_t _cond;static ThreadPool<T> *tp;// c++11的锁static std::mutex _singlock;
};template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;template <class T>
mutex ThreadPool<T>::_singlock;
3.STL,智能指针和线程安全
STL中的容器是否是线程安全的?
不是.
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
智能指针是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
4.其他常见的各种锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
- 自旋锁,公平锁,非公平锁?
这里主要把自旋锁说一说
像以前用的互斥锁或者是信号量,如果申请锁失败了对应的进程就会被挂起,相当于一种阻塞状态或者挂起状态,这种我们一般称为挂起等待锁。
我们一直没有谈论一个问题,下面说一个小故事来谈论这个问题。
场景一:
张三和李四是好朋友,李四是一个学霸喜欢做笔记。马上就期末考试了,张三想找李四借操作系统的笔记学一下,于是就是李四打电话,说请他吃饭等会一起区自习室学习。作为张三的好朋友李四同意了,但是李四说我正在看这本书还要一小时才能看完,你这楼下等等我把。张三有求于李四,于是就同意了,那我就在楼下等等你把。等到20分钟之后李四还没有下来,张三着急了又给李四打电话,问李四你下来没,我在楼下等你呢,李四说还不行还要再等会,张三说行,就把电话挂了又等了一会,等了一会又很着急又给李四打电话,李四你下来没,李四说再等等就好了,一个小时内张三给李四打了上百个电话,最后一次张三给李四打电话,李四说好了好了现在就下来。等下来之后张三李四先去吃饭然后去自习室学习。
场景二:
第二天张三顺利考完操作系统之后觉得考的还行,然后当天中午张三又跑到李四楼下,给李四打电话说昨天你那笔记我们俩用的真不错,马上就要考数据结构了,你能不能把数据结构的笔记借我用一下,李四说我正在楼上看书你能不能再楼下等我一个小时,这时张三就想昨天把我等的太费劲了能不能这样,你忙你的,我去上会网,你块好了你给我打电话,打电话之后我在回来,我就在你宿舍楼下等你然后一块去吃饭再去自习。李四说这个注意不错,这样你也不用给我频繁打电话打扰我,这时张三就很开始就去网吧上网去了,大约过了4、50分钟李四给张三打电话说张三你回来把到我楼下我看好书了,张三就回去了跟李四就一块吃饭然后仔细了。
故事讲完了,下面有些问题
第二个场景:张三去网吧上网等待李四,翻译过来就是当前线程尝试去申请锁一看,上一个线程可能要在临界资源里面待的时间特别久,就将自己挂起等待。
第一个场景:李四好了没好了就下来没好张三就挂电话,李四好了没好了就下来没好张三就挂电话,不断不断的打电话询问条件是否就绪,这种状态我们称之为自旋(就相遇于轮询)。
问题当然不是这个
问:
是什么决定了最终的等待方式?
答:
我们要等待的时长问题!
一个成功申请临界资源的线程在临界区待的时间长短就决定了,我们是选择挂起等待,还是自旋!
未来多线程并发访问共享资源的时候,如果在临界区里面本来待的时间就很久你采用轮询这种方案去检测就不合适,我们选择挂起等待的方式!
那这个时间如何定义呢?
首先这个问题没有标准答案,长或短是对比出来的。如果拘泥于代码方面我们是得不到正确答案的。评估时间长短有两种方案。方案一:结合应用场景。比如说当前一份代码不管是临界区还是非临界区都充满了IO,此时你可以选择挂起等待或者自旋,这看程序员自己,但是一般在临界区里面诸如复杂计算、IO操作、等待某种软件条件等等,大概率都是要挂起等待。如果在临界区里面就是非常简单的内存操作如抢票,代码很短的很快的就能执行完的我们就可以采用自旋方案。但是大部分时候自旋方案我们很少去选,主要因为挂起等待虽然要慢一点,自旋看起要快一些但是自旋一旦你评估时间失误,那么它会大量消耗CPU资源,比如在挂起等待出现死锁大不了我们彼此之间不推荐,但是在自旋出现死锁所以的执行都去疯狂的去自旋检测锁的状态但是没人释放,最后导致我们的CPU瞬间就满了,所以自旋锁相对比较危险的。方案二:都用,分别测试效果,跑一段时间那个效率高就选那个。
下面我们看看自旋接口
pthread_spinlock_t //自旋锁类型
它的用法和互斥锁一样。
初始化销毁
加锁
解锁
下来可以用一下。
4.读者写者问题
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
讲个小故事理解一下。
你们班级有一名女生字写的非常好,图也画的特别好,因此班主任让她去出黑板报,当女孩正在出的时候,大家就跑过去看,盲猜正在画的是什么,一个同学说小花画的是一条蛇,另一个同学说不对小花画的是一条龙,另一个同学也说不对小花画的是一条蜗牛,大家在哪里窃窃私语的讨论小花画的是什么,结果小花最后人家画的是一张世界地图。当小花正在出黑板报的时候其他人在去读取读到的都是内容局部性东西,最终猜的都不对,拿的数据都不正确。为了保证大家读的都是完整的,班里出了规定:当小花出黑板报的时候大家都不能看,小花要么不出,要么就出完,出完再来读。小花出完了大家都去看进行讨论。当大家都在谈论的时候会不会规定这一批都去看黑板把的人大家都排好队一个一个来看,会不会规定谁先来谁先看其他人都把眼睛闭上。当然不会这样规定。其中大部分情况都是一块看。在出黑板报的时候,要么一个人出,要么两个人出一人一半,但是一个人在出的时候不会让另一个人也出,还是要一个一个人来。
现在把这个例子放在一边,我们谈一谈读者写者问题,然后把两者结合一块理解!
读者写者问题我们的分析思路依旧遵守生产者消费者模型321原则。
出黑板报的小花就是写者,这些看的人就是读者,出黑板版本质就是一个读者写者问题。
3种关系:
写者写者之间不能说你正在黑板写字另一个人要把字擦掉要画画,因此写者写者之间是典型的互斥关系。
读者写者之间就像刚才例子,小花正在画其他人就来猜了,这样读不到完整的。另外黑板版画好了大家再读的时候小花就给擦掉了这也不对。所以这是典型的互斥。并且还要一种,这个黑板报是放假之前出的,在开学之前又给擦了,小花出的黑板版没人看是不是没有意义。所以写者写的数据没人读,你又写了也没有什么意思。同样写者写的数据读者多了很长时间,这个数据都陈旧了需要更新了这时是不是该让写者来写。所以读者和写者也有一定的同步关系。
读者读者之间什么关系?
读者和读者没有关系!!彼此之间不需要互斥也不需要同步!
难道小花出好黑板报,大家需要排好队一个一个去看吗,我能看你不能看,根本没有这样的规定。所以读者读者之间没有关系,你读你的我读我的,大家之间互不影响
读者和读者之间为什么没有关系的本质就是:
读者写者模型VS生产者和消费者模型的本质区别是什么?
写者和生产者是一样,主要就是读者和消费者的区别。消费者会拿走数据,读者不会。
那什么场景下会用到这个读者写者模型呢?
- 一次发布,很长时间不做修改,大部分时间都是被读取
如写的博客,发出来之后除非要做修改,大部分时间都是被读的。 - 大部分时间都是在读取,少量的时间在进行写入
下面见见相关接口
pthread_rwlock_t //读写锁
初始化销毁
读加锁
未来读的角色和写的角色是两种角色,读者采用读加锁的方式
写加锁
解锁
无论是读者还是写者最后不想用了都采用这个方案进行解锁
现在就有个问题读写者原理很清楚了,但是读写锁是怎么做到给读加锁写加锁的呢?
不过目前我们知道了,在任意一个时刻,只允许一个写者写入,但是可能允许多个读者读取(写者阻塞)。
写者在写的时候不允许其他写者写也不允许读着读,而读者在读的时候允许其他读者一起读但不允许写着写。
读者写者使用的是不同接口进行加锁它内部是怎么实现的,下面写一点伪代码帮助理解
如果写者先来申请锁成功了,在它正在写入时,读者来了读者肯定也是第一个来的满足if条件要把写者锁关掉,但是写者已经申请锁成功了,读者只能在if内部lock阻塞等待。等到写者写完解锁成功了,读者被唤醒然后继续往下运行。
如果读者先来进来就把写者的锁关闭,其他读者进来就不会在给写者加锁了。只会无脑++,等到退出无脑- -,最后一个人再把写锁解开。在读者正在读的时候写者申请锁就不可能成功。
为什么解锁是同一个函数,加锁两个用的是不同函数。
pthread_rwlock_t //读写锁
因为我们的读写锁是一个结构体所以它的内部可能包含了对应的读锁,写锁,还有读计数器。然后再进行对应的操作时使用特定的一套方案进行相关的操作。
最后再说一点,读者写者中,出现写者饥饿问题很正常,因为本来就是大部分时间都是在读取,少量的时间在进行写入。第一写入线程少,第二写者行为很少人家可能读了一万次你才写一次,所以凭什么让你先写,所以大部分人家去读取让你去饥饿是很正常的。读者写者问题天然就具备写者饥饿的现象。读者来了让读者优先去读这是我们默认的读者优先。读者优先就是读者持续的来就让写者一直等。上面那批接口默认就是读者优先的。
就是想让写者优先也是有场景的,比如说10个读者1个写者,前5个读者已经读了后面陆陆续续还要第6个第7个读者再来的路上,有一次读者和写者一起来了,按照以前读者优先,就让读者先。如果是写者优先,写者拦不住那些已经进去的,但是可以拦住那些还没有进去的,这些还没有进去的线程先别进去,凡是在我写者来的时间点划分,前面读者你先读,后面的读者等一等,等前面读者读完了都走了,先让我们写者进去,写完之后你们在读。
设置读写优先
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
*/
到目前为止系统编程我们就学完啦,真不容易。再写Linux有关博客就是我们的网络编程了,后面再见!