欢迎光临
我们一直在努力

线程池的代码实现

引言

线程池是一种预先创建线程的机制,这些线程在应用程序启动的时候,就已经创建好了,等待着执行线程。当有任务需要执行的时候,线程池会从线程的集合里面分配一个空闲的线程来执行。

这么个操作,有很多的优势:

1、首先就是性能较高,我们不需要频繁的创建和删除线程

2、第二个就是可以控制我们线程的数量,预防不会同时有大量的线程同时存在

3、简化线程的管理,避免了手动管理线程,因为在线程池里面有任务调度的功能

结构

在写代码之前我们还要展现一下线程池的结构:

首先是任务队列,每一个任务进入线程池之后是放在任务队列里面的,而不是直接分配给线程

第二个就是线程容器,这个容器里面存放的就是线程

然后当有空闲的线程的时候,我们就自动从队列里面取出一个任务,从线程容器里面取出一个空闲的线程,让这个线程执行空闲的任务。

代码

首先就是我们装线程的容器选择的是vector,装任务选择队列queue,不过因为我们的任务实际上是一个一个函数,所以我们用c++11提供的std::function<void()>,然后我们还需要一个条件变量,因为我们需要唤醒这个任务队列,在不唤醒的条件下,这些任务都处于睡眠状态。

#ifndef THREADPOOL_H
#define THREADPOOL_H

#include <vector>
#include <thread>
#include <queue>
#include <condition_variable>
#include <mutex>
#include <functional>

class threadPool {
private:
// 线程池相关的私有成员
std::vector<std::thread> threads; // 存储线程的容器
std::queue<std::function<void()>> tasks; // 存储任务的队列
std::mutex mutex; // 互斥锁
std::condition_variable cv; // 条件变量
bool stop; // 是否停止线程池

// 启动线程池
void startThreadPool(size_t threadNum);

public:
threadPool(int threadNum);
~threadPool();

void addTask(std::function<void()> task); // 添加任务到线程池

};

#endif

然后就是构造和析构函数,对于析构函数来说,首先我们对于stop的值更改一定需要原子操作,否则如果被其他线程抢占,很可能会导致多余执行线程,然后这里我们选择最普通的lock_guard锁即可。改变完变量之后我们要唤醒所有的线程,唤醒这些线程之后,在主线程里面采用阻塞回收的方式,把这些线程全部回收(如果有的线程还在工作,那么就阻塞耐心等待)

threadPool::threadPool(int threadNum) : stop(false)
{
startThreadPool(threadNum);
}

threadPool::~threadPool() {
{
std::lock_guard<std::mutex> lock(mutex);
stop = true;
}

// 将所有的等待线程唤醒
cv.notify_all();

// 将所有的工作线程回收(主线程在执行for()循环)
for (auto& thread : threads) {
thread.join();
}
}

然后我们添加一个任务进去,记得添加这个任务的操作也需要加锁,否则很可能对同一个位置添加两个任务。然后随机唤醒一个线程(如果线程都不空闲,那么就立即返回,cv.notify_one()并不是一个阻塞函数)

// 添加任务到线程池
void threadPool::addTask(std::function<void()> task)
{
{
std::lock_guard<std::mutex> lock(mutex);
tasks.push(task);
}

cv.notify_one(); // 唤醒一个等待线程
}

最后我们要启动线程池,这里面的逻辑是先创建一个任务,也就是std::function<void()> task因为要符合我们的参数类型。

在直接取任务之前我们还需要一些操作,首先我们选择加锁,这里我们选择的是std::unique_lock,这个锁有特点就是可以在中间释放,不需要等待生命周期结束,正好符合我们后面的wait的特点。

然后就是阻塞等待了,这个函数在c++11里面加入了lambda表达式,作用就是方式操作系统错误的唤醒某些线程,执行wait时,我们先执行匿名函数,也就是判断当线程池停止工作或者任务队列不为空,如果返回的时true,就拿着这个锁执行后面的内容,如果返回false,放弃这个锁,继续睡觉,直到后面有任务到来把这个激活(nodify)。

那么激活之后,判断一下如果队列为空,且已经通知要销毁这个线程池,那么就return返回销毁这个线程,否则无论如何都要从队列里面取出一个任务,赋值给我们一开始创建的task,来执行。

不过大家还是要注意一个点,就是我们这所有的操作都是在有锁的情况下完成的,而这些操作的目的只有一个,是取任务,不是执行任务,因为如果还要包括执行任务的话,会导致占用锁的时间过长。

所以我们的任务是不包含原子操作的,而这又是一个并发操作,所以任务的原子性,需要我们另外加锁

void threadPool::startThreadPool(size_t threadNum)
{
// 循环创建多个线程
for (size_t i = 0; i < threadNum; i++) {
// 创建一个线程,并将线程放到线程容器之中
threads.emplace_back([this]() { // 把这个类的指针传进去,让函数执行的时候可以捕获到
while (true) {
std::function<void()> task; // 创建一个任务
{
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [this]() {
return stop || !tasks.empty(); // 当线程池停止工作或者任务队列不为空时
});
if (stop && tasks.empty()) {
return;
}

task = std::move(tasks.front()); // 从任务队列中取出一个任务
tasks.pop();
}
task();
}
});
}
}

这个是我们的测试案例

#include "threadPool.h"
#include <iostream>

int main() {
threadPool myThreadPool(4);
std::mutex mutex;
for(int i = 0; i < 20; i++) {
myThreadPool.addTask([i, &mutex](){
{
std::lock_guard<std::mutex> lock(mutex);
std::cout << "task " << std::this_thread::get_id() << std::endl;
}
// 模拟任务执行100毫秒
std::this_thread::sleep_for(std::chrono::milliseconds(500));
});
}

// 等待所有任务执行完成
std::this_thread::sleep_for(std::chrono::seconds(5));

return 0;
}

总结

以上就是我们的一个简单的线程池,线程池是一个非常关键的机制

本篇文章到这里就结束了!!!!希望可以帮助大家更好的理解线程池~~~~

赞(0)
未经允许不得转载:171主机测评 » 线程池的代码实现
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址