目录
1. 线程池
1.1. 什么是线程池
线程虽然比进程轻量了很多,但是每创建一个线程时,需要向操作系统申请空间创建,如果需要开辟大量的线程,申请和销毁的开销也是很大的。所以如果能够提前申请一块空间,专门用来创建线程,那么就能提高一些效率。
线程池:一种线程使用模式,线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过度。
线程池通过一个线程安全的阻塞任务队列加上一个或一个以上的线程实现,线程池中的线程可以从阻塞队列中获取任务进行任务处理,当线程都处于繁忙状态时可以将任务加入阻塞队列中,等到其它的线程空闲后进行处理。
线程池的作用:可以避免大量线程频繁创建或销毁所带来的时间成本,也可以避免在峰值压力下,系统资源耗尽的风险;并且可以统一对线程池中的线程进行管理,调度监控。
线程池的应用场景:
-
需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,可以想象一个热门网站的点击次数。
-
对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
-
接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,导致出现错误
1.2. 创建线程池
其实质原理还是生产者消费者模型,通过一个线程创建任务,将任务放进任务队列中,线程池中的线程竞争任务。
具体实现代码:
task.hpp: (老熟人了)
#pragma once #include <iostream> #include <pthread.h> using namespace std; class Task { private: int _x; int _y; char _op; public: Task() {} Task(int x, int y, char op) : _x(x), _y(y), _op(op) {} int option() { int ans = 0; switch (_op) { case '+': ans = _x + _y; break; case '-': ans = _x - _y; break; case '*': ans = _x * _y; break; case '/': ans = _x / _y; break; case '%': ans = _x % _y; break; default: cout << "error" << endl; break; } cout << "当前任务正在被消费者线程:" << pthread_self() << "处理-->" << _x << _op << _y << "=" << ans << endl; return ans; } int operator()() { return option(); } };
threadpool.hpp:
#pragma once #include<iostream> #include<string> #include<queue> #include<unistd.h> #include<pthread.h> using namespace std; template<class T> class ThreadPool { private: int _capacity; queue<T> _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 capacity = 5):_capacity(capacity) { pthread_mutex_init(&_mtx, nullptr); pthread_cond_init(&_cond, nullptr); } ~ThreadPool() { pthread_mutex_destroy(&_mtx); pthread_cond_destroy(&_cond); } // 在类中要让线程执行类内成员方法是不可行的 // 由于类中非静态成员隐含this指针,这里会导致传参出错,所以需要将该函数设置为静态 static void* Rountine(void* args) { pthread_detach(pthread_self()); ThreadPool<T>*tq = (ThreadPool<T>*) args; while(true) { tq->Lock(); while(tq->IsEmpty()) // 任务队列为空 { tq->Wait(); // 挂起 } T t; tq->PopTask(&t); tq->Unlock(); t(); // 处理任务 //sleep(1); } } void InitThreadPool() { pthread_t tid; for(int i = 0; i < _capacity; ++i) { pthread_create(&tid, nullptr, Rountine, (void*)this); // 由于该执行任务函数为静态函数,不能访问类内成员,所以传参数时,需要传入this指针才能访问到类内成员 } // 不需要记住每个线程的id,创建成功后将每个线程分离,不用手动等待 } void PushTask(const T& in) { Lock(); _task_queue.push(in); Unlock(); Wakeup(); } void PopTask(T* out) { *out = _task_queue.front(); _task_queue.pop(); } };
main.cc:
#include"thread_pool.hpp" #include"task.hpp" #include<ctime> #include<cstdlib> int main() { ThreadPool<Task>* tp = new ThreadPool<Task>; tp->InitThreadPool(); srand((long long)time(nullptr)); while(true) { Task t(rand()%20+1, rand()%10+1,"+-*/%"[rand()%5]); tp->PushTask(t); sleep(1); } }
2.线程安全的单例模式
2.1. 什么是单例模式
一个类,只应该实例化出一个对象,就称为单例。
定义对象的本质,是将对象加载到内存,只让该对象在内存中加载一次,就是单例。
对象被设计成单例的场景:
语义上只需要一个对象
该对象内部存在大量的空间,保存了大量的数据,如果允许该对象存在多份,或者允许发生拷贝,内存中会存在数据冗余。
什么时候加载对象呢?有两种模式:饿汉、懒汉
2.2. 饿汉方式实现单例模式
形象的说就是:吃完饭后,立即就将碗洗了。
对于创建对象层面来说:创建类时,就立即创建出对应的对象。
template <class T>
class Singleton
{
static T data; // 静态成员,该成员属于类,不属于对象,一旦创建了类,该成员就被创建了
public:
static T* GetInstance()
{
return &data;
}
};
template <class T>
T Singleton<T>:: data = T();
只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例。
2.3. 懒汉方式实现单例模式
形象的说就是:吃完饭后,先把碗放下,下一顿饭用到这个碗了再洗碗。
对于创建对象层面来说:延时加载,先不创建对应的对象,等到需要用到的时候才创建。(写时拷贝就用到了这个思想)
template <class T>
class Singleton
{
static T* inst; // 先创建静态成员的指针,指针指向空
public:
static T* GetInstance()
{
if (inst == nullptr)
{
inst = new T(); // 一旦调用了该函数(即需要用到该对象的时候),才创建该对象
}
return inst;
}
};
template <class T>
T* Singleton<T>:: inst = nullptr;
存在一个严重的问题, 线程不安全。
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例。
所以需在创建单例时进行加锁。
2.4. 基于懒汉方式实现单例模式下的线程池
其中的大概框架还是线程池,只是创建对象的方式改变了。
不会在main函数中一开就创建对象,而是在获取到任务之后才创建。并且实现了线程安全。
具体实现代码:task.hpp:老熟人就不放了,上面可以参考
thread_pool.hpp:
#pragma once #include <iostream> #include <string> #include <queue> #include <unistd.h> #include <pthread.h> using namespace std; template <class T> class ThreadPool { private: int _capacity; queue<T> _task_queue; // 临界资源 pthread_mutex_t _mtx; pthread_cond_t _cond; static ThreadPool<T> *ins; private: // 构造函数必须得实现,但是必须私有化(即不能实例化对象) ThreadPool(int capacity = 5) : _capacity(capacity) { pthread_mutex_init(&_mtx, nullptr); pthread_cond_init(&_cond, 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(&_cond, &_mtx); } void Wakeup() { pthread_cond_signal(&_cond); } bool IsEmpty() { return _task_queue.empty(); } public: static ThreadPool<T> *GetInstance() // 设置为静态函数该函数属于类,否则在main函数中,如果未实例化对象将不能调用该函数 { static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; if (ins == nullptr) // 双判定,只有未创建单例对象时才竞争锁,减少锁的争用,提高获取单例的效率 { pthread_mutex_lock(*lock); if (ins == nullptr) // 当前单例对象没有被创建 { ins = new ThreadPool<T>; ins->InitThreadPool(); } pthread_mutex_unlock(&lock); } return ins; } ~ThreadPool() { pthread_mutex_destroy(&_mtx); pthread_cond_destroy(&_cond); } // 在类中要让线程执行类内成员方法是不可行的 // 由于类中非静态成员隐含this指针,这里会导致传参出错,所以需要将该函数设置为静态 static void *Rountine(void *args) { pthread_detach(pthread_self()); ThreadPool<T> *tp = (ThreadPool<T> *)args; while (true) { tp->Lock(); while (tp->IsEmpty()) // 任务队列为空 { tp->Wait(); // 挂起 } T t; tp->PopTask(&t); tp->Unlock(); t(); // 处理任务 // sleep(1); } } void InitThreadPool() { pthread_t tid; for (int i = 0; i < _capacity; ++i) { pthread_create(&tid, nullptr, Rountine, (void *)this); // 由于该执行任务函数为静态函数,不能访问类内成员,所以传参数时,需要传入this指针才能访问到类内成员 } } void PushTask(const T &in) { Lock(); _task_queue.push(in); Unlock(); Wakeup(); } void PopTask(T *out) { *out = _task_queue.front(); _task_queue.pop(); } }; // 初始化类内静态成员 template <class T> ThreadPool<T> *ThreadPool<T>::ins = nullptr;
main.cc:
#include"thread_pool.hpp" #include"task.hpp" #include<ctime> #include<cstdlib> int main() { // ThreadPool<Task>* tp = new ThreadPool<Task>; 单例模式下不能手动创建对象 srand((long long)time(nullptr)); while(true) { Task t(rand()%20+1, rand()%10+1,"+-*/%"[rand()%5]); // 单例本身会在任何场景,任何环境下被调用 // GetInstance 本身可能被多个线程重入,所以会存在线程安全问题 // 所以需在 GetInstance 内部加锁 ThreadPool<Task>::GetInstance()->PushTask(t); // 需要用到该对象的时候才创建 sleep(1); } }