目录
线程池
与之前的生产者消费者模型, 读者写者模型一样, 线程池也是为了解决某些典型场景下的问题而出现的.
举个例子, 当我们双十一疯狂剁手的时候, 假设我们用户的每一笔订单请求在服务器端都要创建一个线程的话, 当11.11号0点
来临, 每一年我们都在刷新着记录, 所以在短时间内会有数量非常非常大的各种任务请求, 如果服务器端要在这些请求到来时
在一个一个的创建线程的话, 需要非常大的时空开销. 其实, 这些线程也不一定要等待大量任务请来的时候才一个个创建, 我
们可以提前创建好解决各种任务的线程, 把他们放在"线程池"中, 当有任务请求来时, 直接将请求给"线程池", "线程池"中的线程
就可以直接完成这个任务请求. 免去了请求高峰时段频繁创建线程而带来的花销.
解决的问题 : 短时间大量任务请求下创建大量线程带来的时空开销, 调度开销, 以及有资源耗尽的风险
优点 : 避免峰值压力下, 资源耗尽的风险. 节省线程创建销毁的时间成本
实现 : 一堆线程(有最大数量限制) + 一个线程安全的任务队列
具体思路 :
思路简单来说, 就是一个ThreadPool的对象中创建许多个线程, 当这个对象的任务等待队列中有任务请求, 也就是push操作时, 唤醒所创建的线程去完成这个任务, 完成任务后, 这个任务就可以pop出任务等待队列. 那么请求任务也不一定是一样的啊, 就比如淘宝来说, 有买的请求, 也有退货的等等, 所以, 我们还需将任务处理的方法与数据封装为一个类ThreadTask, 在线程入口函数中, 只需要调用我们封装的这个类的接口, 不需要关心方法与数据是什么, 保证了我们线程库的实现中, 线程不需要关心要处理什么具体问题, 只需要调用一个固定的接口就可以. 大大降低了耦合性, 我们有不同的任务请求时, 只需要实例化出不同的ThreadTask对象就可以了.
我们来封装一个简单的线程池
ThreadPool.hpp
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
#include<iostream>
#include<pthread.h>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<queue>
using namespace std;
#define MAX_COUNT 10
typedef void (*task_handler_t)(int);
class ThreadTask{
public:
int m_data;
task_handler_t m_handler;
public:
ThreadTask(int data, task_handler_t handler) :
m_data(data), m_handler(handler)
{}
void Run(){
m_handler(m_data);
}
};
class ThreadPool{
size_t m_max_count;//定义线程池中线程的数量
queue<ThreadTask> m_task_q;//任务队列
pthread_mutex_t m_mutex;//互斥锁
pthread_cond_t m_pro_cond, m_con_cond;//条件变量
bool m_exit_flag;//析构标记
vector<pthread_t> m_tid_array;//线程池中所有线程的tid
size_t m_destroy_count;//销毁线程计数
static void* thr_start(void* arg);
void Quit();
public:
ThreadPool(size_t max_count = MAX_COUNT);
~ThreadPool();
bool PushTask(const ThreadTask& task);
};
ThreadPool::ThreadPool(size_t max_count) :
m_max_count(max_count),
m_exit_flag(false),
m_destroy_count(0),
m_tid_array(max_count) {
pthread_mutex_init(&m_mutex, NULL);
pthread_cond_init(&m_con_cond, NULL);
pthread_cond_init(&m_pro_cond, NULL);
int ret;
for(size_t i = 0; i < m_max_count; ++i){
ret = pthread_create(&(m_tid_array[i]), NULL, thr_start, this);
if(ret){
fprintf(stderr, "create thread:%s\n", strerror(ret));
exit(0);
}
//若不关心线程返回值, 并希望线程退出后能够自己释放资源
//pthread_detach(m_tid_array[i]);//则分离这个线程
//否则需要记录所创建所有线程的tid, 再析构时对所有线程pthread_join
}
}
ThreadPool::~ThreadPool(){
Quit();
for(size_t i = 0; i < m_max_count; pthread_join(m_tid_array[i], NULL), ++i);
//detach属性则不需要等待
pthread_mutex_destroy(&m_mutex);
pthread_cond_destroy(&m_pro_cond);
pthread_cond_destroy(&m_con_cond);
}
void* ThreadPool::thr_start(void* arg){
ThreadPool* tp = (ThreadPool*)arg;
while(1){
pthread_mutex_lock(&(tp->m_mutex));
while(tp->m_task_q.empty()){
if(tp->m_exit_flag) {
++(tp->m_destroy_count);
pthread_mutex_unlock(&(tp->m_mutex));
cout<<"线程退出\n";//用于测试
pthread_exit(NULL);
}
pthread_cond_wait(&(tp->m_con_cond), &(tp->m_mutex));
}
ThreadTask task = tp->m_task_q.front();
tp->m_task_q.pop();
pthread_mutex_unlock(&(tp->m_mutex));
task.Run();//要解锁之后再进行任务处理
pthread_cond_signal(&(tp->m_pro_cond));
}
pthread_exit(NULL);
}
bool ThreadPool::PushTask(const ThreadTask& task){
pthread_mutex_lock(&m_mutex);
m_task_q.push(task);
pthread_mutex_unlock(&m_mutex);
pthread_cond_signal(&m_con_cond);
return true;
}
void ThreadPool::Quit(){
pthread_mutex_lock(&m_mutex);
m_exit_flag = true;
pthread_mutex_unlock(&m_mutex);
while(m_destroy_count != m_max_count) {
pthread_cond_broadcast(&m_con_cond);
}
}
#endif
main.cpp
#include<iostream>
#include<cstdio>
#include<pthread.h>
#include"thread_pool.hpp"
void test(int data){
printf("thread:%p get data:%d\n", pthread_self(), data);
}
int main(int argc, char* argv[]){
if(argc != 2){
return -1;
}
ThreadPool pool(4);
for(int i = 0; i < atoi(argv[1]); ++i){
pool.PushTask(ThreadTask(i, test));
}
return 0;
}
线程安全的单例模式
单例模式,属于创建类型的一种常用的设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)
设计模式 : 针对一些经典的常见的场景, 制定的一些对应的解决方案, 就是设计模式.
单例模式的特点
某些类, 只应该具有一个对象(实例), 就称之为单例.
例如 : 一个男人只能有一个媳妇.
针对场景 : 在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据(也就是一份资源只加载一次).
饿汉实现方式和懒汉实现方式
什么是饿汉和饱汉呢 ? 举个例子
吃完饭之后, 马上去洗碗, 下回吃饭之前就不需要洗完了, 这就是饿汉, 即在程序初始化阶段就去加载资源, 后面程序运行只需要使用. 优点 : 运行流畅 缺点 : 初始加载较慢
吃完饭之后, 先不去洗碗, 等到下回吃饭之前再洗, 这就是懒汉, 即程序只有在使用某些资源时才会去加载, 优点 : 初始加载快 缺点 : 运行时可能会会加载资源, 可能会不够流畅
懒汉模式的核心是"延时加载", 从而优化程序的启动速度
饿汉模式实现单例模式
using namespace std;
template<class T>
class singleton{
static T m_data;
public:
T* gey_instance(){
return &m_data;
}
};
template<class T>
T singleton<T>::m_data = 0;
懒汉模式实现单例模式
template<class T>
class singleton{
volatile static T* m_data;
static mutex m_mutex;
public:
volatile T* get_instance(){
if(m_data == nullptr){
m_mutex.lock();
if(m_data == nullptr){
m_data = new int;
}
m_mutex.unlock();
}
return m_data;
}
~singleton(){
if(m_data){
delete m_data;
m_data = nullptr;
}
}
};
template<class T>
volatile T* singleton<T>::m_data = nullptr;
需要注意的是, 懒汉模式存在线程安全的问题, 所以需要实现线程安全.
注意事项:
- 1. 加锁解锁的位置
- 2. 双重 if 判定, 避免不必要的锁竞争
- 3. volatile关键字防止过度优化(保持内存可见性)
STL线程安全的问题
STL在设计之初, 就是为了极致的性能, 而锁对极致性能来说是无情的杀手, 所以就没有实现线程安全. 所以使用STL中的容器
等存在线程安全问题, 需要我们对具体问题, 具体设计, 比如上面的线程池中任务队列的push和pop就需要我们手动实现线程
安全.
智能指针线程安全问题
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量(shared_ptr内部有引用计数), 所以会存在线程安全问题. 但是标准库
实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数. 所以说智能指针
是线程安全的.
智能指针相关博客, 戳链接( ̄︶ ̄)↗ C++ 智能指针(auto_ptr/unique_ptr/shared_ptr)
相关博客, 戳链接( ̄︶ ̄)↗ : Linux 多线程(线程概念/特点/优缺点/与进程比较)
Linux 多线程(线程控制(创建/终止/等待/分离))
Linux 多线程之线程安全(同步与互斥/互斥锁/条件变量/死锁/)
Linux 多线程之线程安全(生产者消费者模型/POSIX信号量/读者写者模型/读写锁/自旋锁)