单例模式概念
单例模式是一种 "经典的, 常用的, 常考的 " 设计模式。
设计模式就是针对不同的情况有与之对应的方法,通俗易懂。
单例模式特点
某些类, 只应该具有一个对象(实例), 就称之为单例。
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。
什么情况下需要单例模式呢?
一般而言,语义上只需要一个,且该对象内部存在大量空间,保存了大量的数据,如果允许拷贝的话,内存中会存在冗余数据。
**总结来说就是:只创建一次,加载一次。**那么什么时候创建或加载呢?有以下两种模式:
饿汉实现方式和懒汉实现方式
饿汉实现方式:在还不需要用到该对象的时候就已经提前创建好了。
懒汉实现方式:什么时候要用了再创建。
举例:将上一部分学习的线程池例子设计成单例模式、
.hpp
#pragma once
#include <iostream>
using namespace std;
#include <pthread.h>
#include <queue>
#include <unistd.h>
namespace ns_thread
{
const int default_num = 5;
pthread_mutex_t mtx;
pthread_cond_t c_cnd;
template <class T>
class ThreadPool
{
private:
//线程池中线程个数
int _num;
queue<T> task_queue;
//静态成员变量要在类外初始化!!
static ThreadPool<T> *ins;
//单例模式,构造函数必须得实现,但是必须的私有化
ThreadPool(int num = default_num)
: _num(num)
{
pthread_mutex_init(&mtx, nullptr);
pthread_cond_init(&c_cnd, nullptr);
}
//拷贝构造
ThreadPool(const ThreadPool<T> &tp) = delete;
ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;
public:
void Lock()
{
pthread_mutex_lock(&mtx);
}
void Unlock()
{
pthread_mutex_unlock(&mtx);
}
void Wait()
{
pthread_cond_wait(&c_cnd, &mtx);
}
void WakeUp()
{
pthread_cond_signal(&c_cnd);
}
bool IsEmpty()
{
return task_queue.empty();
}
public:
static ThreadPool<T> *GetInstance()
{
static pthread_mutex_t _lock = PTHREAD_MUTEX_INITIALIZER;
//双判断,减少锁的征用,提高获取单例的效率
if (ins == nullptr)
{
pthread_mutex_lock(&_lock);
if (ins == nullptr)
{
//初始化
ins = new ThreadPool<T>();
ins->InitThreadPool();
cout << "首次加载对象" << endl;
}
pthread_mutex_unlock(&_lock);
}
return ins;
}
// 在类中要让线程执行类内成员方法,是不可行的!!因为会有隐藏this指针就不符合创建线程的格式了
// 必须让线程执行静态方法,静态成员函数无法访问私有属性
static void *Rountine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp = (ThreadPool<T> *)args;
while (true)
{
tp->Lock();
//加while判断是防止伪唤醒问题:即两个线程都被唤醒了但只有一个拿到了锁,另一个被唤醒因为没抢到锁本该继续挂起等待,
//但因为是if也继续执行下去了.
while (tp->IsEmpty())
{
//挂起等待
tp->Wait();
}
//处理任务
T t;
tp->Pop(&t);
tp->Unlock();
//这里先释放锁再进行任务处理,能达到一个线程处理任务,另一个线程可以抢锁获取数据,达到了并行的效果。
//如果先处理任务再释放锁,那么每个线程处理任务就是串行的,效率肯定没有并行的高
t.Run();
}
}
void InitThreadPool()
{
pthread_t tid;
for (int i = 0; i < _num; i++)
{
pthread_create(&tid, nullptr, Rountine, (void *)this);
}
}
void Push(const T &in)
{
Lock();
task_queue.push(in);
Unlock();
//唤醒线程处理任务
WakeUp();
}
//因为使用Pop函数Rountine已经加锁了,所以不能再Pop函数里再加锁,会造成死锁
void Pop(T *out)
{
*out = task_queue.front();
task_queue.pop();
}
~ThreadPool()
{
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&c_cnd);
}
};
template <class T>
ThreadPool<T> *ThreadPool<T>::ins = nullptr;
}
.cpp
#include <iostream>
#include "thread_pool.hpp"
#include "task.hpp"
#include <time.h>
using namespace std;
using namespace ns_thread;
using namespace ns_task;
int main()
{
srand((long long)time(nullptr));
//因为构造函数设置为私有的就无法初始化了
// ThreadPool<Task> *tp = new ThreadPool<Task>();
// tp->InitThreadPool();
std::cout << "当前正在运行我的进程其他代码..." << std::endl;
std::cout << "当前正在运行我的进程其他代码..." << std::endl;
std::cout << "当前正在运行我的进程其他代码..." << std::endl;
std::cout << "当前正在运行我的进程其他代码..." << std::endl;
std::cout << "当前正在运行我的进程其他代码..." << std::endl;
std::cout << "当前正在运行我的进程其他代码..." << std::endl;
std::cout << "当前正在运行我的进程其他代码..." << std::endl;
while (true)
{
sleep(1);
Task t(rand() % 20 + 1, rand() % 10 + 1, "+-*/%"[rand() % 5]);
ThreadPool<Task>::GetInstance()->Push(t);
//这里会输出ins的地址来展示单例模式的效果
std::cout << ThreadPool<Task>::GetInstance() << std::endl;
}
return 0;
}
tack.hpp
#pragma once
#include<iostream>
using namespace std;
#include<pthread.h>
//实现+-*/%
namespace ns_task
{
class Task
{
private:
int _x;
int _y;
char _c;
public:
Task(){};
Task(int x, int y, char c)
: _x(x), _y(y), _c(c)
{
}
~Task(){};
void Run()
{
int res = 0;
switch (_c)
{
case '+':
res = _x + _y;
break;
case '-':
res = _x - _y;
break;
case '*':
res = _x * _y;
break;
case '/':
res = _x / _y;
break;
case '%':
res = _x % _y;
break;
default:
cout<<"bug??"<<endl;
break;
}
cout<<"当前消费者"<< pthread_self()<<"完成了任务:"<<_x<<_c<<_y<<" = "<<res<<endl;
}
};
}
运行结果如下:
注意:设计成单例模式,构造函数必须得实现,但是必须的私有化,这样就避免了允许拷贝的情况,这是实现单例模式的基本原则。
通过运行结果可以看出,单例模式下使用的线程池只创建了一次,之后每次使用都用的是原来的,没有拷贝复制的情况,避免了数据冗余