线程池简易版
目录
一、什么是线程池
线程池是一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价(一直都是那么几个线程在执行任务,这些线程永远不销毁,一直处于等待任务,处理任务的状态)。线程池不仅能够保证内核的充分利
用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。线程池的应用场景:
(1)需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
(2) 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
(3)接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。线程池最关键的两个步骤:
(1)创建固定数量线程池,循环从任务队列中获取任务对象,
(2)获取到任务对象后,执行任务对象中的任务接口
二、线程池的实现
Thread_Pool.hpp
#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <functional>
#include <thread>
#include <mutex>
#include <condition_variable>
class func_data
{
public:
func_data(int i)
{
std::string name = "thread-";
name += std::to_string(i);
_thread_name = name;
}
std::string get_func_data()
{
return _thread_name;
}
void Print_func_data()
{
std::cout << _thread_name << std::endl;
}
private:
std::string _thread_name;
};
// 将函数类型定义为func_t
using func_t = std::function<void(func_data)>;
class Thread_Pool
{
private:
// 构造
Thread_Pool(size_t thread_num = 5);
// 拷贝构造
Thread_Pool(const Thread_Pool &tp) = delete;
// 赋值运算符重载
const Thread_Pool &operator=(const Thread_Pool &tp) = delete;
public:
//获取单例模式的线程池指针
static Thread_Pool *Get_instance();
// 析构
~Thread_Pool();
// 主线程等待回收子线程
void Wait();
// 主线程插入任务
void Push_task(func_t f);
// 其他线程拿任务
void Get_task(func_data d);
private:
std::queue<func_t> _tasks; // 任务队列
std::vector<std::thread> _workers; // 线程
size_t _threadnum; // 线程数量
std::mutex _mutex; // 取任务时保证线程安全的锁
std::condition_variable _cond; // 有没有任务的条件变量
static Thread_Pool* instance; //单例模式的线程池指针
};
//获取单例模式的线程池指针
Thread_Pool *Thread_Pool::Get_instance()
{
if (instance == nullptr)
{
instance = new Thread_Pool();
std::cout<<"创建单例线程池成功"<<std::endl;
}
return instance;
}
// 其他线程拿任务
void Thread_Pool::Get_task(func_data d)
{
// 要while(1)的原因是,其他线程要不停的从任务队列中拿任务
while (1)
{
// 定义一个可执行对象
func_t fun;
{
// 访问临界区先要线程安全
std::unique_lock<std::mutex> lock(_mutex);
// 拿任务(先要判断有没有任务,没有任务阻塞在条件变量中)
while (_tasks.empty())
{
_cond.wait(lock); // 只能传入一把unique_lock的锁
}
fun = _tasks.front();
_tasks.pop();
}
//可执行对象fun需要一个func_data的参数,这个我们已经传进来了
fun(d);
}
}
// 主线程插入任务
void Thread_Pool::Push_task(func_t in)
{
// 访问临界区先要线程安全
std::unique_lock<std::mutex> lock(_mutex);
// 向队列中插入任务
_tasks.push(in);
// 插入完成唤醒一个线程
_cond.notify_one();
}
// 主线程等待回收子线程
void Thread_Pool::Wait()
{
for (auto &t : _workers)
{
t.join();
}
}
// 析构
Thread_Pool::~Thread_Pool()
{
std::cout << "线程池销毁" << std::endl;
}
// 构造
Thread_Pool::Thread_Pool(size_t thread_num)
: _threadnum(thread_num)
{
// 创建n个线程
for (int i = 0; i < _threadnum; i++)
{
// c++线程库中的构造,第一个参数是可执行对象,后面的都是该可执行对象的参数
// 构造的时候就把任务传进去
// std::thread t(std::bind(&Thread_Pool::Thread_run, this, std::placeholders::_1), func_data(i));
// std::thread t([](func_data d){d.Print_func_data();}, func_data(i));
// 但是我们不想让任务是写死的,想让他从任务队列中拿,我们想用Get_task来获取任务,
// 但是Get_task是一个成员函数,里面有this指针,我们用bind来规避指针(也可以使用静态成员来规避this指针)
// bind的作用是将一个函数绑定,至于新产生的可执行对象有几个参数是看placeholder有几个
// 而thread的第一个参数是可执行对象,第二个参数是该可执行对象的形参
//fd是线程执行函数的参数结构体
func_data fd = func_data(i);
std::thread t(std::bind(&Thread_Pool::Get_task, this, std::placeholders::_1), fd);
// 这里的func_data是一个临时对象,但是我又不想在线程池中用一个vector<func_data来存储数据,所有我被迫把Get_task改成了非引用的样子,幸好我的func_data不大,否则拷贝效率会很低
std::cout << "main线程创建了" << fd.get_func_data() << std::endl;
// 插入到线程池中
_workers.push_back(move(t)); // 这里会发生拷贝构造,但是thread的拷贝构造是被禁用的,所以我们可以使用移动语义
}
}
// 创建线程池单例指针
Thread_Pool* Thread_Pool:: instance=nullptr;
main.cc
#include"Thread_Pool.hpp"
#include<unistd.h>
void f(func_data)
{
std::cout<<gettid()<<"正在执行"<<std::endl;
}
int main()
{
// //创建一个线程池对象,用智能指针去管理
// std::unique_ptr<Thread_Pool> tp(new Thread_Pool(5));
Thread_Pool*tp=Thread_Pool::Get_instance();
//在创建线程池的时候就会去调用Get_task,但是因为没有任务,所以5个线程都阻塞在了_cond条件变量中
//主线程插入任务
while(1)
{
Thread_Pool::Get_instance()->Push_task(f);
// tp->Push_task(f);
sleep(1);
}
Thread_Pool::Get_instance()->Wait();
// tp->Wait();
return 0;
}
以后我们想要给线程池插入不同的任务,只需要写一个函数用来生产void(func_data)即可,这里的func_data也可以自定义,在Thread_Pool.hpp中修改一下即可。
相关说明:
我们创建好了线程池之后,首次我们先是对其进行初始化操作;然后不断的向任务队列塞数据,由线程池中的线程去获取任务并执行相关操作。
1.任务队列(即临界资源)是会被多个执行流同时访问,因此我们需要引入互斥锁对任务队列进行保护。
2.线程池中的线程想要获取到任务队列中的任务,那么就必须要确保任务队列中有任务,所以我们还需引入条件变量来进行判断,如果队列中没有任务,线程池中的线程将会被挂起,直到任务队列中有任务后才被唤醒;
3.在thread_pool.hpp中,多线程去执行对应的方法的时候,采用的是成员函数+bind函数,这样做的目的是解决类中存在隐藏的this指针问题,因为多线程在调用对应的函数时,该函数只有一个形参,不用bind将this指针绑定的话,形参就需要两个参数,是不可以的;所以我们可以将this指针作为参数绑定好,就可以访问类内的成员函数了。
总结:在main创建线程池的时候,需要创建几个线程,而创建线程的时候需要传入可执行对象,这里我们传递的是Thread_Pool的类内方法,而这个方法,执行的是不断从任务队列中拿任务,然后执行。bind之后其实就相当于在类外定义了了一个方法。
三、单例模式
单例(Singleton)模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例 ;
使用场景:
1.语义上只需要一个
2.该对象内部存在大量的空间,保存了大量的数据,如果允许该对象存在多份,或者允许发生各种拷贝,内存中存在冗余数据;
一般Singleton模式通常有三种形式:饿汉式:吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭。
懒汉式:吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式。
懒汉方式最核心的思想是 "延时加载"。(例如我们之前所学过的写时拷贝)从而能够优化服务器的启动速度。
(1)饿汉模式
(2)懒汉模式
四、在线程池的基础上修改成懒汉式单例模式
最后在类外定义静态成员
原文地址:https://blog.csdn.net/2303_79336820/article/details/144265007
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!