基于C++11 实现的线程池

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


基于C++11 实现的线程池

  C++11加入了线程库,这是历史性的一步跨越,因为它已然能够实现简单的并发了,但有这样一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

  如何解决这种线程的频繁创建销毁呢?就是线程执行完一个任务,不会被销毁,而是可以继续执行其他的任务呢?

  一个好的办法就是使用线程池,不过线程池是个什么东西呢?它又该怎么实现呢?

1、线程池原理

  线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件), 则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

2、线程池的设计思路?

实现线程池有以下几个步骤:

(1)设置一个生产者消费者队列,作为临界资源。

(2)初始化n个线程,并让其运行起来,加锁去队列里取任务运行

(3)当任务队列为空时,所有线程阻塞。

(4)当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程来处理。

3、一个基于C++11的优秀的线程池

  • 此线程池来源于github上一个6.1k star的线程池项目:
  • 源码地址:https://github.com/progschj/ThreadPool

3.1 头文件

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex> // 锁
#include <condition_variable> // 条件变量
#include <future>
#include <functional>
#include <stdexcept>

  头文件中,没有什么特别的,这里稍微说下:thread—线程相关的库,mutex—互斥量,也就是互斥锁,condition_variable—条件变量,用于唤醒线程和阻塞线程,future的话,在这里就是一个用来获取线程数据的函数。

  对于C++11的并发编程部分(std::thread, std::future等)可以参考下面几篇文章进行学习:

  • 货比三家:C++ 中的 task based 并发:https://segmentfault.com/a/1190000002706259
  • 从 pthread 转换到 std::thread:https://segmentfault.com/a/1190000002655852

3.2 线程池类

  线程池类中:

  • 首先是一个构造函数,参数size_t是所传入的线程数。
  • 然后是一个enqueue的模板函数,emmm…,大佬就是大佬,这个函数真是大肠包小肠,具体看一看,其需要传入的参数是一个函数和函数参数,然后返回的是一个future,最后还通过一个type进行检测。
  • workers是工作线程
  • tasks是任务队列
class ThreadPool {
    
    
public:
    ThreadPool(size_t);
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type>;
    ~ThreadPool();
private:
   	// 需要跟踪线程,以便我们可以加入它们
    std::vector< std::thread > workers;
    // 任务队列
    std::queue< std::function<void()> > tasks;
    // 同步
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

3.3 构造函数实现

  这个构造函数主要是用来启动一些工作线程,咋一看还不一定能看的懂,下面我们详细分析:

  • 首先在初始化列表中初始化stopfalse
  • 然后是一个线程数的for循环,循环里是一个很长的workers.emplace_back()语句,就是添加线程到工作线程组。再往里面则是一个lambda表达式。
  • lambda的函数体第一层是一个无线循环,用于线程内不断的从任务队列取任务执行,第二层有一个task就是一个函数对象,然后又一个大括号,刚开始我没看明白,但往下看就能懂,在这个大括号(大括号作用:控制lock临时变量的生命周期,执行完毕或return后自动释放锁)里面有:
    • std::unique_lock<std::mutex> lock(this->queue_mutex);加锁
    • this->condition.wait(lock, [this]{ return this->stop || !this->tasks.empty(); }); 等待条件成立(当线程池被关闭或有可消费任务时跳过wait继续;否则condition.wait将会unlock释放锁,其他线程可以继续拿锁,但此线程会阻塞此处并休眠,直到被notify_*唤醒,被唤醒时wait会再次lock并判断条件是否成立,如成立则跳过wait,否则unlock并休眠继续等待下次唤醒)
    • condition只是返回了false才会wait,也就是!stop && empty才会wait
// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads)
    :  stop(false)
{
    
    
    for(size_t i = 0;i<threads;++i)
        workers.emplace_back(
            [this]
            {
    
    
                for(;;)
                {
    
    
                    std::function<void()> task; //线程中的函数对象
                    // 通过lock互斥获取一个队列任务和任务的执行函数
                    {
    
    
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        this->condition.wait(lock,
                            [this]{
    
     return this->stop || !this->tasks.empty(); });
                        if(this->stop && this->tasks.empty())
                            return;
                        // 任务队列的队首任务
                        task = std::move(this->tasks.front());
                        // 从队列移除
                        this->tasks.pop();
                    }
                    // 调用函数执行任务
                    task();
                }
            }
        );

}

3.4 入队—添加新的工作任务到线程池

  我们重新审视std::queue< std::function<void()> > tasks;,它只能接受void的函数类型,这里使用std::packaged_task<return_type()>完成函数类型的推导,因为这还不是最终放入tasks的对象,它要承接一个返回future的工作,而package_task就是来打包返回future的。

  其次将任务函数和其参数绑定,构建一个packaged_task(packaged_task是对任务的一个抽象,咱们能够给其传递一个函数来完成其构造。以后将任务投递给任何线程去完成,经过packaged_task.get_future()方法获取的future来获取任务完成后的产出值)

// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) 
    -> std::future<typename std::result_of<F(Args...)>::type>
{
    
    
    using return_type = typename std::result_of<F(Args...)>::type;
    auto task = std::make_shared< std::packaged_task<return_type()> >(  //指向F函数的智能指针
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)  //传递函数进行构造
        );
    // 获取任务的future
    std::future<return_type> res = task->get_future();
    {
    
    
        // 独占拿锁
        std::unique_lock<std::mutex> lock(queue_mutex);

        // 不允许入队到已经停止的线程池 
        if(stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");
        // 将任务添加到任务队列
        tasks.emplace([task](){
    
     (*task)(); });
    }
    // 发送通知,唤醒一个wait状态的工作线程重新抢锁并重新判断wait条件
    condition.notify_one();
    return res;
}

3.5 析构函数

// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    
    
    {
    
    
        std::unique_lock<std::mutex> lock(queue_mutex); // 拿锁
        stop = true; // 停止标志
    }
    condition.notify_all(); // 条件变量进行通知
    // 等待所有工作线程结束
    for(std::thread &worker: workers)
        worker.join();
}

3.6 测试代码

#include <iostream>
#include <vector>
#include <chrono>

#include "ThreadPool.h"

int main()
{
    
    
    
    ThreadPool pool(4);
    std::vector< std::future<int> > results;

    for(int i = 0; i < 8; ++i) {
    
    
        results.emplace_back(
            pool.enqueue([i] {
    
    
                std::cout << "hello " << i << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(1));
                std::cout << "world " << i << std::endl;
                return i*i;
            })
        );
    }

    for(auto && result: results)
        std::cout << result.get() << ' ';
    std::cout << std::endl;
    
    return 0;
}

3.7 演示

  • 演示平台:腾讯云服务器 2 核 2 G,系统 Centos 7.6
  • 编译环境:GCC 9.1.0
    在这里插入图片描述

期待大家和我交流,留言或者私信,一起学习,一起进步!

猜你喜欢

转载自blog.csdn.net/CltCj/article/details/128384041