简易线程池

目录

简易线程池代码

线程基本概念

线程池的基本概念

改进方向

方案一:引入C++20协程 (但是好像协程可以与多进程搭配并发)

方案二:分离I/O密集和计算密集任务


简易线程池代码

// https://github.com/illjlack/demo
#pragma once

#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);
    ~ThreadPool();

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type>;  
        // 推导返回值类型 (是future包装的F(Args...)的返回值的类型)
        // std::future,表示一个异步操作的结果,可以在任务完成后获取返回值。

private:
    std::vector<std::thread> workers;             // 工作线程
    std::queue<std::function<void()>> tasks;      // 任务队列

    std::mutex queue_mutex;                       // 任务队列的互斥锁,保护任务队列的访问
    std::condition_variable condition;            // 条件变量(队列为空或者线程池暂停)
    bool stop;                                    // 标志变量,表示线程池是否停止
};


// 构造函数实现:启动指定数量的线程
// 使用内联, 防止链接后重复定义
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;

                    {   // 锁的作用域
                        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(); // 执行任务
                }
            }
        );
    }
}

// 析构函数实现:停止所有线程并等待它们完成任务
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true; // 设置停止标志
    }
    condition.notify_all(); // 通知所有线程
    for (std::thread &worker : workers)
    {
        worker.join(); // 等待线程完成后关闭线程
    }
}

// `enqueue` 方法实现:向线程池的任务队列中添加新任务
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
    -> std::future<typename std::invoke_result<F, Args...>::type>
{
    using return_type = typename std::invoke_result<F, Args...>::type;

    // 将任务包装成 `std::packaged_task`,以便获取任务的返回值
    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(); // 获取任务的 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; // 返回任务的 future 对象
}

使用demo

#include <iostream>
#include <vector>
#include <chrono>
#include "ThreadPool.h"


int main()
{
    // 创建一个包含4个线程的线程池
    ThreadPool pool(4);

    // 存储每个任务的结果
    std::vector<std::future<int>> results;

    // 向线程池添加8个任务
    for (int i = 0; i < 8; ++i)
    {
        results.emplace_back(
            pool.enqueue([i]
            {
                std::cout << "Task " << i << " started" << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(1));
                std::cout << "Task " << i << " finished" << std::endl;
                return i * i;  // 返回 i 的平方
            })
        );
    }

    // 获取并打印每个任务的结果
    for (auto && result : results)
    {
        // result.get() 是阻塞的
        std::cout << "Result: " << result.get() << std::endl;
    }

    return 0;
}

线程基本概念

线程是操作系统调度的基本单位,它是一个独立的执行路径,与进程共享相同的内存空间和资源。每个线程可以独立执行任务,但在多线程环境下,线程之间可能需要同步或通信,这就带来了上下文切换的开销。

  1. 线程有自己的函数栈: 每个线程在执行时拥有自己独立的函数栈,用于保存局部变量、返回地址等执行上下文。

  2. 多线程能够利用CPU的多内核并行执行: 现代处理器通常有多个内核,多个线程可以在不同的内核上同时执行,实现真正的并行处理,从而提高程序的执行效率。

线程池的基本概念

线程池是一种用于管理和复用线程的技术。通过预先创建一定数量的线程来执行任务,而不是每次执行任务时都创建新的线程,从而节省资源并提高性能。

  1. 线程创建的开销:

    • 每次创建线程需要分配函数栈空间(通常默认大小为 1M)以及其他资源。

    • 使用线程池可以避免频繁创建和销毁线程的开销,重复利用已有的线程来执行任务。

  2. 线程切换的开销:

    • 线程切换涉及上下文切换,包括保存和恢复寄存器状态、切换内核态和用户态等,这些操作都是有开销的。

    • 线程池通过控制线程的数量,避免线程过多导致频繁的上下文切换,从而降低系统的开销,提高任务执行的效率。

改进方向

线程池的工作原理可以类比成为多批次流水线处理

I/O阻塞可能耗费很长时间,导致线程空闲等待,降低整体的处理效率。

方案一:引入C++20协程 (但是好像协程可以与多进程搭配并发)

思路: 通过C++20的协程功能,将I/O阻塞操作进行挂起处理,当I/O操作完成后再恢复执行。这样可以避免线程在等待I/O操作时被阻塞,进而更高效地利用线程资源。

  • 利用 co_awaitco_yield 等协程关键字,将长时间等待的I/O操作挂起。

  • 通过编写自定义的协程调度器来管理协程的执行。当I/O操作未完成时,将协程挂起,释放线程的控制权。

优点:

  • 高效资源利用:线程不会因为I/O阻塞而闲置,可以继续处理其他任务。

  • 简化并发模型:协程提供了类似同步的编程风格,简化了异步I/O的处理逻辑。

缺点:

  • 引入复杂性:协程的引入可能需要对现有代码结构进行较大调整。

  • 协程上下文切换:虽然轻量,但仍然存在一些开销。

方案二:分离I/O密集和计算密集任务

思路: 将线程池分为两个部分:一个专门处理I/O密集型任务(这些任务可能会因为I/O操作而长时间阻塞),另一个专门处理计算密集型任务(这些任务需要大量CPU计算但不涉及长时间等待)。

  • I/O线程池:专门处理I/O密集型任务。这个线程池中的线程可以被设计为等待I/O完成后再继续处理任务。

  • 计算线程池:专门处理计算密集型任务。这个线程池的线程会一直执行CPU密集型任务,不会被I/O操作阻塞。

  • 任务在提交到线程池之前,需要根据其特性(I/O密集或计算密集)进行分类,分别提交到不同的线程池中。对于混合型任务(既包含I/O操作,又包含计算任务),可以拆分成两个子任务,分别提交到相应的线程池。

  • I/O线程池同步用condition_variable主动沉睡到被唤醒 或 std::this_thread::yield()主动让出时间片等。

...       (不太懂,不会。。。)

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值