历经整整一个月终于到linux系统知识的最后一篇博客了,在这期间博主承认有偷懒几天~这篇博客写完后,接下来就是C++的高阶数据结构了。等博主把网络编程和MySql学好之后再继续写~今天主要介绍线程池、单例模式、读者写者模型、悲观锁和自旋锁的区别。
目录
线程池
为什么要有线程池?
代码测试
thread_pool.hpp
task.hpp
main.cc
Makefile
单例模式
什么是单例模式?
什么是设计模式?
单例模式特点
饿汉实现方式和懒汉实现方式
饿汉模式实现单例
懒汉模式方式单例模式
懒汉模式实现线程池版本代码
signal_pool.hpp
task.hpp
main.cc
读者写者模型
基本理论
函数接口
pthread_rwlock_init
pthread_rwlock_destroy
pthread_rwlock_wrlock
pthread_rwlock_rdlock
phread_rwlock_unlock
如何理解?伪代码
挂起等待的锁 vs 自旋锁
自旋锁函数接口
pthread_spin_init
pthread_spin_destroy
pthread_spin_lock
pthread_spin_unlock
我们先类比一下内存池:
我们如果频繁向OS申请小块空间,OS就要在底层做很多动作,比如进程身份状态的变化,OS执行内存处理算法。这些对于用户层来说都是没必要的,但确实是耗时间的!所以引入了内存池的概念,趁着OS好的时候,一次申请一大块空间,这块空间在用户层进行管理,这样当我们再次申请空间时,就不需要向OS去要了,直接在用户层拿就好了,这样效率大大提高。
再来看线程池:
我们发现当多个任务到来时,OS要创建线程,但是临时创建的话效率肯定是比较低的。所以要提前创建好线程,保存在在线程池中。
#pragma once
#include
#include
#include
#include
using namespace std;
namespace ns_threadpool
{
const int g_num = 3; //线程池中线程的数目
template
class ThreadPool
{
private:
int _num; //线程池中线程的数目
queue _task_queue;//该成员是一个临界资源
pthread_mutex_t _mtx;
pthread_cond_t _cond;
public:
void Lock()
{
pthread_mutex_lock(&_mtx);
}
void Unlock()
{
pthread_mutex_unlock(&_mtx);
}
void Wait()
{
pthread_cond_wait(&_cond, &_mtx);
}
void WakeUp()
{
pthread_cond_signal(&_cond);
}
bool IsEmpty()
{
return _task_queue.empty();
}
public:
ThreadPool(int num = g_num)
:_num(num)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_cond, nullptr);
}
//在类中要让线程执行类内成员方法(参数个数匹配问题,只有一个参数),是不可行的!
//必须让线程执行静态方法
static void* Rountine(void* args)
{
pthread_detach(pthread_self());//线程分离
ThreadPool* tp = (ThreadPool*)args;
while(true)
{
tp->Lock();
while(tp->IsEmpty())
{
//任务队列为空,线程该做些什么呢?
tp->Wait();
}
//到这里,该任务队列中一定有任务了
T t;
tp->PopTask(&t);
tp->Unlock();
t();
}
}
void InitThreadPool()
{
pthread_t tid;
for(int i = 0; i < _num; i++)
{
pthread_create(&tid, nullptr, Rountine, (void*)this);
}
}
void PushTask(const T& in)
{
Lock();
_task_queue.push(in);
Unlock();
WakeUp();
}
//由于queue大小可以动态增长,这里不考虑满了就等待的的情况
//这里不能加锁,否则就成死锁了,Rountine也有锁,但是这里也是安全的~
void PopTask(T* out)
{
*out = _task_queue.front();//尾插头出
_task_queue.pop(); //尾插头出
}
~ThreadPool()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_cond);
}
};
}
#pragma once
#include
#include
#include
#include "thread_pool.hpp"
#include "task.hpp"
#include
#include
#include
using namespace ns_task;
using namespace ns_threadpool;
int main()
{
ThreadPool* tp = new ThreadPool(3);
tp->InitThreadPool();
srand((unsigned int)time(0));
while(true)
{
//生产任务
int x = rand()%20 + 1;
int y = rand()%10 + 1;
char op = "+-*/%"[rand()%5];
Task t(x, y, op);
//放任务
tp->PushTask(t);
sleep(1);
}
return 0;
}
main:main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f main
测试结果:
这时候我们发现,我们不断往任务队列中塞任务,我们在线程池中创建的多个线程不断去处理。
单例模式是一种 "经典的, 常用的, 常考的" 设计模式。
IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重.。为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式。
某些类, 只应该具有一个对象(实例), 就称之为单例。
例如一个男人只能有一个媳妇.
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。
洗完的例子:
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭。
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式。
总结:懒汉方式最核心的思想是 "延时加载",从而能够优化服务器的启动速度。这和我们之前讲过的内存申请和内存使用及写时拷贝的概念类似。
通过Singleton 这个包装类来使用T对象, 则一个进程中只有一个T对象的实例。
注意:
存在一个严重的问题, 线程不安全。第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例。
#pragma once
#include
#include
#include
#include
using namespace std;
namespace ns_threadpool
{
const int g_num = 3; //线程池中线程的数目
template
class ThreadPool
{
private:
int _num; //线程池中线程的数目
queue _task_queue; //该成员是一个临界资源
pthread_mutex_t _mtx;
pthread_cond_t _cond;
static ThreadPool* _ins;
private:
//构造函数必须得实现,而且设置为私有
ThreadPool(int num = g_num)
: _num(num)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_cond, nullptr);
}
//拷贝构造和赋值设置为私有
ThreadPool(const ThreadPool& tp) = delete;
ThreadPool operator=(const ThreadPool& tp) = delete;
public:
static ThreadPool* GetInstance()
{
//静态锁不用手动初始化和销毁
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
//当前单例对象还没有被创建
if(_ins == nullptr) //双判定,减少锁的争用,提高单例的效率
{
pthread_mutex_lock(&lock);
if(_ins == nullptr)
{
_ins = new ThreadPool();
_ins->InitThreadPool(); //创建线程
cout << "首次加载对象" << endl;
}
pthread_mutex_unlock(&lock);
}
return _ins;
}
//在类中要让线程执行类内成员方法(参数个数匹配问题,只有一个参数),是不可行的!
//必须让线程执行静态方法
static void *Rountine(void *args)
{
pthread_detach(pthread_self()); //线程分离
ThreadPool *tp = (ThreadPool *)args;
while (true)
{
tp->Lock();
while (tp->IsEmpty())
{
//任务队列为空,线程该做些什么呢?
tp->Wait();
}
//到这里,该任务队列中一定有任务了
T t;
tp->PopTask(&t);
tp->Unlock();
t();
}
}
void InitThreadPool()
{
pthread_t tid;
for (int i = 0; i < _num; i++)
{
pthread_create(&tid, nullptr, Rountine, (void *)this);
}
}
void PushTask(const T& in)
{
Lock();
_task_queue.push(in);
Unlock();
WakeUp();
}
//由于queue大小可以动态增长,这里不考虑满了就等待的的情况
//这里不能加锁,否则就成死锁了,Rountine也有锁,但是这里也是安全的~
void PopTask(T *out)
{
*out = _task_queue.front(); //尾插头出
_task_queue.pop(); //尾插头出
}
~ThreadPool()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_cond);
}
public:
void Lock()
{
pthread_mutex_lock(&_mtx);
}
void Unlock()
{
pthread_mutex_unlock(&_mtx);
}
void Wait()
{
pthread_cond_wait(&_cond, &_mtx);
}
void WakeUp()
{
pthread_cond_signal(&_cond);
}
bool IsEmpty()
{
return _task_queue.empty();
}
};
template
ThreadPool* ThreadPool::_ins = nullptr;
}
#pragma once
#include
#include
#include
#include "thread_pool.hpp"
#include "task.hpp"
#include
#include
#include
using namespace ns_task;
using namespace ns_threadpool;
int main()
{
cout << "当前正在运行我的进程其他代码..." << endl;
cout << "当前正在运行我的进程其他代码..." << endl;
cout << "当前正在运行我的进程其他代码..." << endl;
cout << "当前正在运行我的进程其他代码..." << endl;
cout << "当前正在运行我的进程其他代码..." << endl;
sleep(3);
while(true)
{
//生产任务
int x = rand()%20 + 1;
int y = rand()%10 + 1;
char op = "+-*/%"[rand()%5];
Task t(x, y, op);
Task t1(rand()%20+1, rand()%10+1, "+-*/%"[rand()%5]);
ThreadPool::GetInstance()->PushTask(t);
//单例模式本身会在任何场景,任何环境下使用
//GetInstance():被多线程重入,进而导致线程安全问题
//所以要加锁
cout << ThreadPool::GetInstance() << endl;
sleep(1);
}
return 0;
}
运行结果:
我们发现每次都是一个只有对象,这个对象的线程池里面有多个线程执行任务。
pthread_rwlock_t *restrict rwlock:传入定义的pthread_rwlock_t变量的地址
const pthread_rwlockattr_t *restrict attr:设置属性,我们不关心,传nullptr就好了
pthread_rwlock_t* rwlock:传入变量的地址,进行释放锁资源
以写方式加锁
以读者身份加锁
解锁,读者写者以统一方式解锁。
优先级
读者优先:读者和写者同时到来的时候,我们让读者先进入访问。
写者优先:当读者和写者同时到来的时候,比当前写者晚来的所有的读者,都不要进入临界区访问了,等临界区中没有读者的时候,让写者先写入。
注意:读者多,写着少的问题,是存在"饥饿问题",但是,"饥饿问题"是一个中性词。
我们先找一个现实场景:
对于临界资源任务处理时间比较短就不适合使用挂起等待的锁,因为可能还没有线程挂起等待,别的线程就释放锁了,这时候线程还得继续挂起然后立刻被唤醒,这是有一定成本的!
如果对于处理任务比较长的时候,不适合自旋锁,自旋锁不停地循环检测锁的状态,长时间的话消耗CPU资源也是很大的。
所以针对不同的场景,要选用合适的锁
线程如何得知,自己在临界资源中呆多长时间?
线程不知道!!程序员知道!所以是程序员选择锁的使用!
pthread_spinlock_t* lock:传入定义的自旋锁的变量的地址
int pshared:是否进程间共享,我们一般设置成0
pthread_spinlock_t* lock:传入定义的自旋锁的变量的地址,释放锁资源
pthread_spin_lock:传入定义的自旋锁的变量的地址,进行加锁
pthread_spin_lock:传入定义的自旋锁的变量的地址,进行解锁
我们发现自旋锁和互斥锁的使用几乎一模一样,有了之前的基础,我们就可以使用起来自旋锁。
看到这里,给博主点个赞吧~