线程池(基于C++11实现)

目录

本文中涉及的C++11特性 完整AC代码(GitHub)

1.线程池的设计

线程池的基本思想是预先创建一定数量的线程,并将它们放入一个池中。线程池负责管理线程的生命周期,并将任务分配给空闲的线程。当任务到达时,线程池会从池中取出一个空闲线程来执行任务。当任务完成后,线程会返回到池中,等待下一个任务的到来。这种方式可以避免频繁地创建和销毁线程,从而提高系统的性能和资源利用率。

如果要编写一个线程池,需要考虑以下几个关键点:

  1. 线程池的管理器:负责创建、销毁线程、维护线程状态。
  2. 任务队列:用于存储执行的任务,任务通常以函数对象(如std::function)的形式存储。
  3. 工作线程:线程池中的实际线程,它们从任务队列中取出任务并执行。
  4. 同步机制:为了确保线程安全,需要使用互斥锁、条件变量等同步机制来保护共享资源。

在设计线程池时,还需要考虑以下几个方面:

  1. 线程池的大小管理:
    1. 固定大小:线程池中线程的数量是固定的,不会动态增加或减少,适用于负载相对稳定的场景。
    2. 动态调整:格局任务负载动态调整线程池的大小。适用于负载变化较大的场景。
  2. 任务队列管理:
    1. FIFO队列:最常用的任务队列实现方式,按照任务的提交顺序执行任务。
    2. 优先级队列:根据任务的优先级执行任务,适用于需要按优先级处理任务的场景。
  3. 线程安全:
    1. 互斥锁:用于保护共享资源(如任务队列)的访问。
    2. 条件变量:用于线程之间的通信,如通知空闲线程有新的任务到来。
    3. 原子变量:对原子变量的操作都是原子操作,它是线程安全的。
  4. 任务执行与错误处理:
    1. 任务执行过程中,如果发生异常,需要正确处理异常,避免程序崩溃。

image-20

2.使用C++11实现线程池

2.1 头文件

#include <thread>
#include <vector>
#include <queue>
#include <map>
#include <atomic>
#include <functional>
#include <condition_variable>
#include <mutex>
#include <future>
#include <memory>
#include <iostream>

/* 
    构成:
        1.管理者线程: 子线程 1个
            控制线程池的创建、销毁、线程数量的调整
        2.工作线程: 子线程 n个
            从任务队列中取出任务,并处理
            任务队列为空,被阻塞(被条件变量)
            线程同步(互斥锁)
            当前数量,空闲的线程数量
            最小、最大线程数量
        3.任务队列: stl->queue
            互斥锁
            条件变量
        4.线程池开关 bool
*/

class ThreadPool
{
public:
    ThreadPool(int min = 2, int max = std::thread::hardware_concurrency());
    ~ThreadPool();
    void addTask(std::function<void(void)> task); // 添加任务 -> 任务队列

private:
    void manager(void);
    void worker(void);
private:
    std::queue<std::function<void(void)>> m_tasks;
    std::mutex m_queueMutex;
    std::mutex m_idsMutex;
    std::conditon_variable m_condition;
    std::atomic<bool> m_stop;
    std::thread *m_manager;
    std::map<std::thread::id, std::thread> m_workers;
    std::vector<std::thread::id> m_ids;
    std::atomic<int> m_minThread;
    std::atomic<int> m_maxThread;
    std::atomic<int> m_curThread;
    std::atomic<int> m_idleThread;
    std::atomic<int> m_exitThread;
};

相关函数介绍:

  • 构造函数:初始化线程池,并创建管理器线程、工作线程。
  • 析构函数:销毁线程池,等待所有工作线程退出;设置 m_stop 标志位 true ,通知所有工作线程退出,并释放资源。
  • std::thread::hardware_concurrency() 是C++标准库中 std::thread 类的一个静态成员函数,用于获取系统支持的并发线程数。
  • manager():管理者线程的任务函数。
  • worker():工作线程的任务函数。
  • addTask(std::function<void(void)> task) 将任务添加到任务队列中,并通知工作线程有新任务到来。

2.2 线程池实现

#include "ThreadPool.h"

ThreadPool::ThreadPool(int min, int max) : m_maxThread(max), m_minThread(min), m_stop(false), m_curThread(min), m_idleThread(min)
{
    m_manager = new std::thread(&ThreadPool::manager, this);

    for(int i = 0; i < min; ++i) {
        std::thread t(&ThreadPool::worker, this);
        m_workers.insert(std::maker_pair(t.get_id(), std::move(t)));
    }
}

ThreadPool::~ThreadPool()
{
    m_stop = true;
    m_condition.notify_all();

    if((*m_manager).joinable()) 
    {
        (*m_manager).join();
        delete ma_manager;
        m_manager = nullptr;
    }

    for(auto &it : m_workers)
    {
        std::thread &t = it.second;
        if (t.joinable())
        {
            std::cout << "***************线程 " << t.get_id() << " 将要退出了 ...\n";
            t.join();
        }
    }
    m_workers.clear();
}

void ThreadPool::addTask(std::function<void(void)> task)
{
    {
        std::lock_guard<std::mutex> lock(m_queueMutex);
        m_tasks.push(task);
    }

    m_condition.notify_one();
}

void ThreadPool::manager()
{
    while(!m_stop.load())
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        int idle = m_idleThread.load();
        int cur = m_curThread.load();
        if(idle > cur / 2 && cur > m_minThread)
        {
            m_exitThread = cur - m_minThread;
            m_condition.notify_all();

            std::lock_guard<std::mutex> lock(m_idsMutex);
            for(auto id : m_ids)
            {
                auto it = m_workers.find(id);
                if(it != m_workers.end())
                {
                    std::cout << "----------线程 " << (*it).first << " 被销毁了 ...\n";
                    (*it)->second.join();
                    m_workers.erase(it);
                }
            }
            m_ids.clear();
        } else if(idle == 0 && cur < m_maxThread) {
            std::thread t(&ThreadPool::worker, this);
            m_workers.insert(std::make_pair(t.get_id(), std::move(t)));
            m_curThread++;
            m_idleThread++;
        }
    }
}

coid ThreadPool::worker()
{
    while(!m_stop.load())
    {
        std::function<void(void)> task = nullptr;

        {
            std::unique_lock<std::mutex> locker(m_queueMutex);
            while (m_tasks.empty() && !m_stop.load())
            {
                m_condition.wait(locker);
                if(m_exitThread.load() > 0)
                {
                    m_curThread--;
                    m_idleThread--;
                    m_exitThread--;
                    std::cout << "----------线程退出了,ID: " << std::this_thread::get_id() << std::endl;
                    std::lock_guard<std::mutex> lock(m_idsMutex);
                    m_ids.emplace_back(std::this_thread::get_id());
                    return;
                }
            }

            if(!m_tasks.empty())
            {
                std::cout << "取出了一个任务..." << std::endl;
                task = move(m_tasks.front());
                m_tasks.pop();
            }
        }

        if(task)
        {
            m_idleThread--;
            task();
            m_idleThread++;
        }
    }
}

使用C++11中提供的类,整体代码比C语言风格API代码更简洁,可读性更强。

  • void worker() 工作线程的任务函数
    • 工作的线程在该函数内部通过 while (!m_stop.load()) 循环重复的进行 从任务队列取数据 -> 处理数据 的操作。
    • 如果线程池没关闭并且任务队列为空,工作线程被条件变量阻塞。
    • 通过 addTask 往任务队列中添加新任务之后,可以唤醒被条件变量阻塞的线程。
  • void manager() 管理线程的任务函数
    • 通过睡眠的方式,每3秒检查一次线程池中的线程数量,如果空闲线程数量大于总线程数量的一半,并且当前线程数量大于最小线程数量,则销毁多余的线程。
    • 如果空闲线程数量为0,并且当前线程数量小于最大线程数量,则创建新的工作线程。
  • void addTask(std::function<void(void)> task)
    • 添加新的任务到任务队列中,并 m_condition.notify_one 唤醒一个被阻塞工作线程。

测试代码:

void calculate(int x, int y)
{
    int z = x + y;
    std::cout << "z = " << z << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
}

int main()
{
    ThreadPool pool(2, 24);
    for(int i = 0; i < 10; ++i)
    {
        auto obj = std::bind(calculate, i, i * 2);
        pool.addTask(obj);
    }

    std::cout << "按回车退出..." << std::endl;
    getchar();

    return 0;
}
  • 关于main() 函数中的测试代码
    • 通过添加任务函数 addTask 可以得知任务函数返回值类型是 void 并且无参,但是测试程序中添加的任务函数 calculate 有两个参数,很显然和任务函数的参数是类型是不匹配的,因此需要使用 std::bindcalculate 函数和参数绑定起来。
auto obj = std::bind(calculate, i, i * 2);

这样得到的可调用对象就是无参的。通过这种方式表面看起来只能存储无参函数的任务队列,就可以存储带任意参数的有参函数了。

  • getchar() 是一个阻塞函数,当程序执行到 getchar() 时,程序会等待用户输入,用户输入之后,程序才会继续执行,主线程执行完毕,主进程也就退出了。
posted @ 2026-07-15 19:05  Jaylan  阅读(9)  评论(0)    收藏  举报