希望我们不仅把编程当成一份工作,更要将其当成一份热爱!!!
-----潜意识中有个想成为一名厉害的程序员的梦
什么是线程池
就是在一个池子(一个类)里有几个已经被创建的线程(线程就是用来执行代码的执行流),他们不断的检测任务队列是否有新任务(任务实际上就是代码块、即函数),当队列中有新任务时,空闲线程将其取出并执行。且线程在执行完当前任务后,不会被销毁,可以继续等待队列中有新任务时,将其取出执行。
典型的生产者消费者模式。
线程池的优势
- 当有新任务时,无需新建线程,利用线程池现存的空闲线程执行,减少新建线程的开销。
- 当有新任务到达,且存在空闲线程的时候,因为不用再新建线程,所以提高了响应时间。
- 线程执行完当前任务后,无需销毁,继续复用给未来的任务,减少销毁线程的开销。
线程池类的实现
核心成员变量
- 线程集合(采用c++11标准库的thread)
- 任务队列(任务实际上指的就是函数,这里采用function来接收任务(函数))
核心方法
- 构造函数:设置线程池线程数量,新建线程并绑定其工作函数。
- 线程工作函数:循环检测队列是否有任务,有任务则提取进行执行(核心)。
- 任务提交函数:提交任务到任务队列。
- 停止函数:控制线程池的停止。
- 析构函数:等待所有线程执行完毕。
代码如下:
/*
ClassName: threads_pool
Description : 手写简单线程池
Author : Select and Strive
Date : 2024 - 07 - 20
Version 1.0
to be improved:加入条件变量,避免循环造成的资源浪费
动态化调整线程的数量
*/
#pragma once
#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <functional>
using namespace std;
//生产者消费者模式
//核心类成员:线程集合,任务集合
//核心成员函数:线程工作函数(循环读取任务集合,并执行任务),任务集合提交函数
class threads_pool
{
public:
threads_pool():threads_num(10), stop(false){
init();
}
//设置线程的个数。
threads_pool(unsigned int threads_num_param ):threads_num(threads_num_param ), stop(false) {
init();
}
~threads_pool(){
//如果不在此处等待线程完毕,那么主线程结束后,所有线程池的子线程会被迫结束,导致任务没有被执行完毕。
//加了这段代码,在主线程退出后,会调用线程池的析构函数,停在这里等待线程执行完任务后停止(前提时,stop函数必须被调用,否则线程不会结束)
for (auto& temp:threads_vec)
{
temp.join();
}
}
//任务提交
void task_submit(function<void()> task)
{
mtx_q.lock();
tasks_que.push(task);
mtx_q.unlock();
}
void pool_stop()//所有任务都已经提交后,才能调用此函数。
{
//当队列为空,即所有任务都被线程取走的时候试着stop为true。这样当线程执行完所有任务后,即可停止。
while (!tasks_que.empty()){}
stop.store(true, memory_order_release);
}
private:
//初始化
void init()
{
//绑定线程的工作函数
for (size_t i = 0; i < threads_num; i++)
{
threads_vec.emplace_back(&threads_pool::thread_RunTaskFromQueue,this);
}
}
//线程工作函数
void thread_RunTaskFromQueue()
{
while (true&&!stop.load(std::memory_order_acquire)) //利用.load(std::memory_order_acquire)方式可以保证从内存中读取数据,而不是从cpu缓存中读取数据
{
function<void()> temp(nullptr);
mtx_q.lock();
//如果非空,则提取任务。
if(!tasks_que.empty())
{
temp = tasks_que.front();
tasks_que.pop();
}
mtx_q.unlock();
//如果提取了任务,则执行
if (temp)
{
temp();
}
}
}
vector<thread> threads_vec;//线程池集合
queue<function<void()>> tasks_que;//任务队列
unsigned int threads_num;//线程池数量
mutex mtx_q;//任务队列互斥锁
atomic<bool> stop;//控制线程池的停止
};
额外说明
- 为了防止多线程同时访问共享资源(任务队列)出现问题,对其访问的时候要加互斥锁。
- 为了保证调用停止功能(设置stop变量为true)后,线程的工作函数能及时读到其更新后的值(数据可见性问题),采用 atomic<bool>来定义stop。利用.store(true, memory_order_release)方法来设置stop为true,可以保证更新stop的值后立即被写入内存,而不是cpu缓存(cpu本身的缓存策略,导致更改的值不会第一时间更新到内存,而是更新到缓存);利用.load(std::memory_order_acquire)方式可以保证从内存中读取数据,而不是从cpu缓存中读取数据(cpu的缓存机制会导致其从缓存中读取数据以至于不是最新的数据)。
- 这里停止函数的实现逻辑是:首先停止函数是在所有任务都加入任务队列后才能被调用,这样在函数中检查任务队列为空,即所有任务都被线程所提取的时候,设置stop为true。那么当此时正在执行任务的线程执行完任务后,下一次循环时,检测stop为true则跳出循环,结束线程。所以调用完停止函数后,并不是立刻停止,而是等待所有任务全部执行完才会停止。
- 如果没有停止函数的话,因为线程的工作函数是个死循环,那么它就会一直执行。
- 在析构函数中一定要等待所有线程执行完毕,否则当主线程退出后,线程还没有将任务执行完,就会被迫退出。
测试代码如下:
#include <iostream>
#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include "threads_pool.h"
using namespace std;
std::mutex cout_mutex;
int main()
{
threads_pool my_threads_pool(3);
size_t i = 0;
for (; i < 6; i++)
{
my_threads_pool.task_submit([i]()
{
cout_mutex.lock();
cout << "任务" << i<<"正在运行" << endl;
cout_mutex.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
cout_mutex.lock();
cout << "任务" << i << "运行结束" << endl;
cout_mutex.unlock();
}
);
}
std::this_thread::sleep_for(std::chrono::seconds(5));
for (; i < 12; i++)
{
my_threads_pool.task_submit([i]()
{
cout_mutex.lock();
cout << "任务" << i << "正在运行" << endl;
cout_mutex.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
cout_mutex.lock();
cout << "任务" << i << "运行结束" << endl;
cout_mutex.unlock();
}
);
}
my_threads_pool.pool_stop();
}
待提高
- 加入条件变量,避免循环造成的资源浪费
- 动态化调整线程的数量