C++ 线程池

1. C++ 线程池

什么是线程池

线程池(Thread Pool)是一种线程管理机制,主要用于限制系统中线程的数量、重用线程资源以及减少线程频繁创建和销毁的开销。它通过维护一个线程集合(通常是固定数量的工作线程),让这些线程来执行任务而不必为每个任务都创建一个新线程。

线程池的核心组件
  • 任务队列:保存需要执行的任务,通常是一个线程安全的队列(如 std::queuestd::deque)。
  • 工作线程:线程池中预先创建的线程,用于从任务队列中取任务并执行。
  • 任务提交接口:用于提交任务到线程池,通常使用 std::functionstd::packaged_task 等形式来封装任务。
  • 同步机制:用来协调任务队列和工作线程,如互斥锁(std::mutex)和条件变量(std::condition_variable)。
C++线程池的实现

一个简单的 C++ 线程池实现通常包括以下步骤:

  1. 创建若干个工作线程(线程池启动时创建)。
  2. 每个工作线程循环等待任务队列中的任务。
  3. 当有新任务提交时,将任务放入任务队列并通知空闲线程。
  4. 工作线程从任务队列取出任务并执行。
  5. 线程池关闭时,停止所有工作线程。

示例代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 

class ThreadPool {
public:
    ThreadPool(size_t numThreads);
    ~ThreadPool();

    template 
    auto enqueue(F&& f, Args&&... args) -> std::future::type>;

private:
    std::vector workers;  // 工作线程
    std::queue> tasks;  // 任务队列

    std::mutex queueMutex;  // 任务队列的互斥锁
    std::condition_variable condition;  // 条件变量,用于通知工作线程有任务可执行
    bool stop;  // 标记线程池是否停止
};

// 构造函数:创建指定数量的线程,并让它们等待任务
ThreadPool::ThreadPool(size_t numThreads) : stop(false) {
    for (size_t i = 0; i < numThreads; ++i) {
        workers.emplace_back([this] {
            while (true) {
                std::function task;
                {
                    std::unique_lock lock(this->queueMutex);
                    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 
auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future::type> {
    using return_type = typename std::result_of::type;

    auto task = std::make_shared>(std::bind(std::forward(f), std::forward(args)...));

    std::future res = task->get_future();
    {
        std::unique_lock lock(queueMutex);
        tasks.emplace([task]() { (*task)(); });
    }
    condition.notify_one();  // 通知工作线程有任务可执行
    return res;
}

// 析构函数:停止所有工作线程
ThreadPool::~ThreadPool() {
    {
        std::unique_lock lock(queueMutex);
        stop = true;
    }
    condition.notify_all();
    for (std::thread& worker : workers) worker.join();
}

int main() {
    ThreadPool pool(4);

    auto result = pool.enqueue([](int a, int b) { return a + b; }, 5, 10);
    std::cout << "Result: " << result.get() << std::endl;  // 输出15

    return 0;
}
优点
  • 减少线程创建销毁的开销:线程池中的线程是复用的,避免了频繁的线程创建和销毁。
  • 提高资源利用率:限制并发线程的数量,防止系统因线程过多而导致资源耗尽。
  • 任务提交简单:通过封装任务函数,可以异步提交任务,并获取结果。
常见面试问题
  • 线程池如何管理任务队列?
  • 线程池如何实现负载均衡?
  • 线程池中的线程是如何被回收和销毁的?
  • 如何处理线程池中的异常?

2.线程池与协程

相同点
  1. 提高并发能力

    • 协程线程池的主要目的之一都是提高程序的并发能力,以便更好地利用系统资源。
    • 协程通过允许在单个线程中执行多个任务(通过协同切换)来实现并发。
    • 线程池通过复用固定数量的线程来执行多个任务,避免频繁的线程创建和销毁。
  2. 资源节约

    • 两者都旨在减少系统资源开销。
    • 协程的上下文切换比线程更轻量,因为它不需要操作系统级别的调度。它通过用户态的方式进行调度,避免了线程上下文切换带来的系统调用开销。
    • 线程池通过避免频繁创建和销毁线程,减少了线程的创建、销毁和调度的开销。
  3. 适用于I/O密集型任务

    • 协程非常适合I/O密集型任务,如网络请求、文件操作等。协程允许在执行I/O操作时暂停执行,而不阻塞线程,因此可以在单个线程上处理大量异步I/O操作。
    • 线程池也常用于处理I/O密集型任务,避免为每个I/O操作创建新线程。当一个任务等待I/O时,线程池中的其他线程可以继续处理其他任务,最大化CPU利用率。
  4. 任务的调度与管理

    • 协程线程池都需要某种形式的任务调度系统。协程通过程序员显式地使用 co_await 等机制进行调度,而线程池则使用任务队列和工作线程调度任务。
    • 它们都解决了如何管理并发任务、如何避免任务间的冲突以及如何有效地分配资源的问题。
  5. 任务复用与共享

    • 协程通过在单个线程中切换多个任务,充分利用CPU时间片,避免CPU等待阻塞操作。
    • 线程池通过多个任务共享一组工作线程,避免线程资源的浪费。
不同点
  1. 调度机制

    • 协程使用的是协作式调度,由程序员显式地控制任务的暂停和恢复(例如通过 co_await)。它依赖于任务本身来决定什么时候暂停和恢复,通常是单线程内的调度。
    • 线程池中的线程使用的是抢占式调度,由操作系统来管理线程的调度。每个线程在时间片耗尽或被I/O阻塞时会被操作系统中断和切换。
  2. 上下文切换的开销

    • 协程上下文切换非常轻量级,只涉及在用户态保存和恢复函数的执行状态。
    • 线程池的线程上下文切换涉及到内核态和用户态之间的切换,开销相对较大,尤其是在任务频繁切换的情况下。
  3. 执行模型

    • 协程一般是在一个线程内执行多个任务,通过手动切换来让每个任务分时执行。
    • 线程池是在多个线程上并发执行多个任务,多个线程可以真正同时运行(在多核处理器上)。
  4. 应用场景

    • 协程更适合用于高并发的I/O密集型场景,如异步网络服务、异步文件系统操作等。
    • 线程池更适合 CPU 密集型任务或者需要并行处理的场景,适合需要真正并行的任务(如大规模计算、数据处理等)。
  5. 可扩展性

    • 协程可以在单个线程上处理大量任务,避免过多线程带来的资源浪费,特别是在任务主要是等待I/O的时候。
    • 线程池则可以根据系统资源的情况动态调节线程的数量,支持多核处理器的并行任务处理。

 

你可能感兴趣的:(C++,基础知识,c++)