C++实现线程池

本文转载自:https://blog.csdn.net/caoshangpa/article/details/80374651

1、为什么需要线程池技术

    目前的大多数网络服务器,包括Web服务器、Email服务器以及数据库服务器等都具有一个共同点,就是单位时间内必须处理数目巨大的连接请求,但处理时间却相对较短。传统多线程方案中我们采用的服务器模型则是一旦接受到请求之后,即创建一个新的线程,由该线程执行任务。任务执行完毕后,线程退出,这就是是“即时创建,即时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于不停的创建线程,销毁线程的状态。我们将传统方案中的线程执行过程分为三个过程:T1、T2、T3。
    T1:线程创建时间;
    T2:线程执行时间,包括线程的同步等时间;
    T3:线程销毁时间;
    那么我们可以看出,线程本身的开销所占的比例为(T1+T3) / (T1+T2+T3)。如果线程执行的时间很短的话,这比开销可能占到20%-50%左右。如果任务执行时间很长的话,这笔开销将是不可忽略的。
    除此之外,线程池能够减少创建的线程个数。通常线程池所允许的并发线程是有上界的,如果同时需要并发的线程数超过上界,那么一部分线程将会等待。而传统方案中,如果同时请求数目为2000,那么最坏情况下,系统可能需要产生2000个线程。尽管这不是一个很大的数目,但是也有部分机器可能达不到这种要求。
    因此线程池的出现正是着眼于减少线程本身带来的开销。线程池采用预创建的技术,在应用程序启动之后,将立即创建一定数量的线程(N1),放入空闲队列中。这些线程都是处于阻塞(Suspended)状态,不消耗CPU,但占用较小的内存空间。当任务到来后,缓冲池选择一个空闲线程,把任务传入此线程中运行。当N1个线程都在处理任务后,缓冲池自动创建一定数量的新线程,用于处理更多的任务。在任务执行完毕后线程也不退出,而是继续保持在池中等待下一次的任务。当系统比较空闲时,大部分线程都一直处于暂停状态,线程池自动销毁一部分线程,回收系统资源。基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的任务上,执行次数越多,每个任务所分担到的线程本身开销则越小,不过我们另外可能需要考虑进去线程之间同步所带来的开销
     事实上,线程池并不是万能的。它有其特定的使用场合。线程池致力于减少线程本身的开销对应用所产生的影响,这是有前提的,前提就是线程本身开销与线程执行任务相比不可忽略。如果线程本身的开销相对于线程任务执行开销而言是可以忽略不计的,那么此时线程池所带来的好处是不明显的,比如对于FTP服务器以及Telnet服务器,通常传送文件的时间较长,开销较大,那么此时,我们采用线程池未必是理想的方法,我们可以选择“即时创建,即时销毁”的策略。
总之线程池通常适合下面的几个场合:

(1) 单位时间内处理任务频繁而且任务处理时间短;
(2) 对实时性要求较高。如果接受到任务后在创建线程,可能满足不了实时要求,因此必须采用线程池进行预创建。

2、代码实现(参考GitHub

    线程池的实现思想:“管理一个任务队列,一个线程队列,然后每次取一个任务分配给一个线程去做,循环往复。”

(1)头文件ThreadPool.h的内容如下

#ifndef THREAD_POOL_H
#define THREAD_POOL_H
 
#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>
 
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;            //线程队列,每个元素为一个Thread对象
    std::queue< std::function<void()> > tasks;     //任务队列,每个元素为一个函数对象    

    std::mutex queue_mutex;                        //互斥量
    std::condition_variable condition;             //条件变量
    bool stop;                                     //停止
};
 
// 构造函数,把线程插入线程队列,插入时调用embrace_back(),用匿名函数lambda初始化Thread对象
inline ThreadPool::ThreadPool(size_t threads) : stop(false){

    for(size_t i = 0; i<threads; ++i)
        workers.emplace_back(
            [this]
            {
                for(;;)
                {
                    // task是一个函数类型,从任务队列接收任务
                    std::function<void()> task;  
                    {
                        //给互斥量加锁,锁对象生命周期结束后自动解锁
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        
                        //(1)当匿名函数返回false时才阻塞线程,阻塞时自动释放锁。
                        //(2)当匿名函数返回true且受到通知时解阻塞,然后加锁。
                        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();                      // 执行这个任务
                }
            }
        );
}
 
// 添加新的任务到任务队列
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()> >(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );
        
    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)(); });          //把任务加入队列
    }                                                   //自动解锁
    condition.notify_one();                             //通知条件变量,唤醒一个线程
    return res;
}
 
// 析构函数,删除所有线程
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for(std::thread &worker: workers)
        worker.join();
}
 
#endif

(2)基本使用方法

#include <iostream>
#include "ThreadPool.h"
 
int main()
{
    // create thread pool with 4 worker threads
    ThreadPool pool(4);
 
    // enqueue and store future
    auto result = pool.enqueue([](int answer) { return answer; }, 42);
 
    // get result from future, print 42
    std::cout << result.get() << std::endl; 
}

(3)另一个例子

#include <iostream>
#include "ThreadPool.h"
 
void func()
{
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout<<"worker thread ID:"<<std::this_thread::get_id()<<std::endl;
}
 
int main()
{
    ThreadPool pool(4);
    while(1)
    {
       pool.enqueue(fun);
    }
}

3、代码详细解释:

    首先我们看析构函数:inline ThreadPool::ThreadPool(size_t threads) : stop(false){},这个函数向线程队列worker插入threads个线程,每个线程对象用一个匿名函数lambda来初始化,每个匿名函数会一直循环着从任务队列中取出任务来执行。它首先对互斥量加锁,获得互斥锁后,调用条件变量的wait()函数等待条件发生,传入wait()函数的第二个参数为一个匿名函数lambda,当函数返回值为false时,wait会使得该线程阻塞(任务队列为空时会阻塞),并且对互斥量进行解锁。当函数返回值为true(任务队列不为空)并且收到条件变量的通知后,wait函数使线程解阻塞,并且对互斥量加锁。接着从任务队列里面弹出一个任务,对互斥量自动解锁,并执行这个任务。

    接着我们看添加任务的函数:auto ThreadPool::enqueue(F&& f, Args&&... args)-> std::future<typename std::result_of<F(Args...)>::type>{},这个函数向任务队列中加入一个任务。它首先创建一个名为task的智能指针,接着对互斥量加锁,然后把该任务加入到任务队列中,对互斥量自动解锁。调用条件变量的notify_one()函数,使得阻塞到这个条件变量的线程解阻塞。

    总结:初始化时,线程中的每个函数都会阻塞到条件变量那里,当任务队列中新加入一个任务时,通知阻塞到条件变量的某一个线程,接着这个线程执行:互斥量加锁——>任务队列出队——>互斥量解锁——>执行任务。当线程执行完任务之后,如果任务队列不为空,则继续从任务队列那里取出任务执行,如果任务队列为空则阻塞到条件变量那里。

  (1) std::result_of<func()>::type:获取func()函数返回值的类型;

  (2) typename:作用是告诉c++编译器,typename后面的字符串为一个类型名称,而不是成员函数或者成员变量;

  (3) 匿名函数lambda: 允许incline函数的定义被当做一个参数,用法为:[]()->type{},中括号内指定了传递方式(值传递或引用传递),小括号内指明函数的形参,->type指定了返回值类型,花括号内指定了函数体。

  (4) std::funtion<void()> :声明一个函数类型,接收任意原型是void()的函数、或函数对象、或是匿名函数。void() 意思是不带参数,没有返回值。

  (5) std::unique_lock<> : C++多线程编程中通常会对共享的数据进行写保护,以防止多线程在对共享数据成员进行读写时造成资源争抢导致程序出现未定义的行为。通常的做法是在修改共享数据成员的时候进行加锁--mutex。在使用锁的时候通常是在对共享数据进行修改之前进行lock操作,在写完之后再进行unlock操作,进场会出现由于疏忽导致由于lock之后在离开共享成员操作区域时忘记unlock,导致死锁。针对以上的问题,C++11中引入了std::unique_lock与std::lock_guard两种数据结构。通过对lock和unlock进行一次薄的封装,实现对象生命周期结束后自动解锁的功能,参考。用法如下:
    1)新建一个unique_lock对象 :std::mutex mymutex; 
    2)给对象传入一个std::mutex 对象作为参数:unique_lock lock(mymutex);

  (6) std::condition_variable:当 std::condition_variable 对象的某个 wait 函数被调用的时候,它使用 std::unique_lock(通过 std::mutex) 来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程。
    std::condition_variable::wait() :std::condition_variable提供了两种 wait() 函数:
    1) 第一种情况:void wait (unique_lock<mutex>& lock);当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lock),直到另外某个线程调用 notify_* 唤醒了当前线程。在线程被阻塞时,该函数会自动调用 lock.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_* 唤醒了当前线程),wait() 函数也是自动调用 lock.lock(),使得 lock 的状态和 wait 函数被调用时相同。
   2) 第二种情况:void wait (unique_lock<mutex>& lock, Predicate pred)(即设置了 Predicate),只有当 pred 条件为 false 时调用 wait() 才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞。
   std::condition_variable::notify_one() :唤醒某个等待(wait)线程。如果当前没有等待线程,则该函数什么也不做,如果同时存在多个等待线程,则唤醒某个线程是不确定的;

  (7) using return_type = typename xxx :指定别名,和typedef的作用类似;

  (8) std::make_shared<type> : 创建智能指针,需要传入类型;

  (9) std::future<> : 可以用来获取异步任务的结果,因此可以把它当成一种简单的线程间同步的手段;

  (10) std::packaged_task<> : std::packaged_task 包装一个可调用的对象,并且允许异步获取该可调用对象产生的结果,从包装可调用对象意义上来讲,std::packaged_task 与 std::function 类似,只不过 std::packaged_task 将其包装的可调用对象的执行结果传递给一个 std::future 对象(该对象通常在另外一个线程中获取 std::packaged_task 任务的执行结果);

  (11) std::bind() : bind的思想实际上是一种延迟计算的思想,将可调用对象保存起来,然后在需要的时候再调用。而且这种绑定是非常灵活的,不论是普通函数、函数对象、还是成员函数都可以绑定,而且其参数可以支持占位符,比如你可以这样绑定一个二元函数auto f = bind(&func, _1, _2);,调用的时候通过f(1,2)实现调用;

  (12) std::forward<F> : 左值与右值的概念其实在C++0x中就有了。概括的讲,凡是能够取地址的可以称之为左值,反之称之为右值,C++中并没有对左值和右值给出明确的定义,从其解决手段来看类似上面的定义,当然我们还可以定义为:有名字的对象为左值,没有名字的对象为右值。std :: forward有一个用例:将模板函数参数(函数内部)转换为调用者用来传递它的值类别(lvalue或rvalue)。这允许右值参数作为右值传递,左值作为左值传递,一种称为“完全转发”的方案;

  (13) std::packged_task::get_future: 获取与共享状态相关联的 std::future 对象。在调用该函数之后,两个对象共享相同的共享状态。

参考:https://www.cnblogs.com/haippy/p/3284540.html

<think>好的,我现在需要帮用户解决如何用C++实现线程池的问题。首先,我得回顾用户提供的引用内容,看看里面有哪些关键点。引用1到4提到了不同的实现方法,比如使用vector存储线程、构造函数中的初始化、任务队列的管理,还有析构函数中的资源回收。用户需要具体的代码示例和实现方法,所以我得把这些信息整合起来。 首先,线程池的基本组成部分包括任务队列、工作线程和管理线程。引用3提到使用TaskQueue来管理任务,工作线程从队列中取任务执行。构造函数里初始化线程数组和管理线程,析构函数负责安全关闭。这点很重要,用户可能需要知道如何正确释放资源,避免内存泄漏。 然后,引用2中的构造函数示例展示了如何根据硬件并发数创建线程,并在lambda函数中处理任务循环。这可以帮助用户理解工作线程的逻辑结构。同时,引用4提到销毁时的冲突问题,需要处理队列和线程的同步,可能需要互斥锁和条件变量来管理。 接下来,我需要考虑代码的结构。通常线程池需要一个任务队列,用互斥锁保护,条件变量来通知线程有新任务。工作线程循环等待任务,处理完任务后继续等待。管理线程可能用于动态调整线程数量,如引用3中的例子,但用户可能只需要基础实现,所以可以先简化这部分。 用户可能需要一个简单但完整的示例,所以我要确保代码涵盖必要的部分:任务提交、线程启动、任务处理、资源回收。同时,需要解释关键部分,比如如何停止线程池,如何处理未完成的任务。 可能的问题点包括:线程安全的任务队列、正确使用条件变量避免死锁、在析构函数中通知所有线程退出。引用4提到了clear和shutdown的冲突,所以需要确保在停止时先清空队列,再通知线程退出。 现在,我需要将这些元素整合成一个示例代码。使用C++11或更高版本的特性,比如std::thread、std::mutex、std::condition_variable。任务队列可以用std::queue,用lambda函数作为任务类型。构造函数中创建一定数量的工作线程,每个线程循环等待任务。添加任务时,锁定互斥锁,将任务加入队列,然后通知一个线程。析构函数设置停止标志,通知所有线程,并join它们。 还需要处理异常情况,比如线程池已停止时不能再添加任务。用户可能想知道如何扩展功能,比如动态调整线程数量,但基础示例可能不需要这些,可以作为相关问题提出。 最后,检查代码是否符合引用中的最佳实践,比如使用RAII管理资源,正确锁定作用域,避免死锁。确保示例代码简洁,易于理解,同时覆盖关键实现步骤。</think>### C++线程池实现方法详解 #### 一、基本结构 线程池核心包含任务队列、工作线程组、同步机制三部分。以下为实现要点: 1. **任务队列**:使用`std::queue<std::function<void()>>`存储待执行任务 2. **线程管理**:`std::vector<std::thread>`维护工作线程组 3. **同步机制**:`std::mutex`与`std::condition_variable`实现线程间通信 #### 二、完整代码示例 ```cpp #include <vector> #include <thread> #include <queue> #include <mutex> #include <condition_variable> #include <functional> #include <future> class ThreadPool { public: explicit ThreadPool(size_t threads = std::thread::hardware_concurrency()) : stop(false) { for(size_t i = 0; i < threads; ++i) { workers.emplace_back([this] { while(true) { std::function<void()> task; { 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(); } }); } } template<class F, class... Args> auto 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()>>( std::bind(std::forward<F>(f), std::forward<Args>(args)...)); 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)(); }); } condition.notify_one(); return res; } ~ThreadPool() { { std::unique_lock<std::mutex> lock(queue_mutex); stop = true; } condition.notify_all(); for(std::thread &worker : workers) if(worker.joinable()) worker.join(); } private: std::vector<std::thread> workers; std::queue<std::function<void()>> tasks; std::mutex queue_mutex; std::condition_variable condition; bool stop; }; ``` #### 三、实现要点解析 1. **线程初始化**:构造函数中根据硬件并发数创建线程组,每个线程执行任务循环[^2] 2. **任务提交**:`enqueue`方法支持任意可调用对象,返回`std::future`获取异步结果 3. **任务执行**:工作线程通过条件变量唤醒,获取任务后释放锁执行[^3] 4. **安全停止**:析构函数先设置停止标志,再唤醒所有线程等待退出[^4] #### 四、使用示例 ```cpp #include <iostream> int main() { ThreadPool pool(4); for(int i = 0; i < 8; ++i) { pool.enqueue([i] { std::cout << "Task " << i << " executed by thread " << std::this_thread::get_id() << std::endl; }); } return 0; } ```
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值