线程池的目的:
1.复用线程,减少频繁创建和销毁的开销
创建和销毁线程是昂贵的系统操作,涉及内核调度、内存分配;
使用线程池预先创建一批线程,在多个任务间循环复用,避免资源浪费,提高性能。
2.主线程不阻塞,让异步线程处理任务
主线程可以专注于接收任务或调度逻辑;
实际的耗时操作(如 IO、计算)由线程池中的工作线程执行;
这样可以提升程序响应速度、增强并发能力。
3.控制线程数量,避免线程过多导致资源竞争
线程池原理:
任务队列+一组工作线程+条件变量调度机制(唤醒阻塞的工作线程处理任务)
1.任务队列 1.提供push放入任务的接口 2.pop获取任务的接口(没有 工作线程会阻塞) 3.cancel析构线程池 通过mutex保证同步+condition_variable控制工作线程
2.调度器 1.初始化创建工作线程 2.提供工作线程的入口函数work()从队列中循环pop获取任务处理 没有任务会阻塞的pop函数中。3.提供Post 主线程向队列中放入任务的接口。
单生产者对多消费者(单队列)
1.调度器ThreadPool
1.构造函数
创建一定数量的工作线程 并进行管理。
2.Woker() 工作线程的入口函数
循环处理队列中的任务,通过Pop()获取任务 输出型参数task带回任务,没有任务就回阻塞在Pop()函数中。
3.Post() 向队列中放入任务的接口
bind绑定函数 将闭包对象存入任务队列中
4.析构函数
对每个退出的线程进行join()等待回收
2.任务队列
1.Push()入队列接口
先加锁 把任务放入到队列中 解锁后再用条件变量唤醒一个工作线程,进行处理。
2.Pop() 出队列
输出型参数value 返回给上层任务。条件变量阻塞线程,等有队列有任务才会解除阻塞 或者退出的时候。
3.Cancel()
线程池析构逻辑,设置为非阻塞_nonblock=true,并唤醒所有线程 进行退出。
此时在Pop()被阻塞的线程被唤醒 返回false,工作线程break退出循环。
多生产者对多消费者(双队列)
上面单队列的适合一个生产者对应多个消费者的场景,如果多对多 那锁的冲突就大了。对此我们采用的是双缓冲区的策略,即生产者和消费者各一个队列。
工作线程消费完工作队列中的数据后 会进行交换队列的操作,继续进行处理。
1.调度器
不改变也没问题,但对于这个向队列中放入任务的Post()函数 原本只能存入std::function<void()>类型的函数。为了解决函数有返回值,上层获取到返回值,我们这样设置Post函数。
这是一个线程池任务提交函数(Post),它接受任意可调用对象
f
和参数args...
,并返回一个std::future
,用于异步获取结果。1.实现要解决 传入的函数F的返回值类型是什么。可以用类型萃取工具std::invoke_result_t<>获取F函数 参数Args...的返回值类型。但参数的类型等模板推导完才行,因此采用了
auto post()->类型 尾置返回值类型的语法(告诉编译器等模板推导完再 告诉你返回值的类型)
2.现在知道了返回值类型,怎么把返回值给上层?
1.promise+futrue 在任务函数中手动设置结果。但还要更好的选择
2.packaged_task<>+future异步任务封装器,对函数进行封装 返回闭包对象。让线程执行这个闭包对象 等函数执行完自动把结果进行设置,就不用手动设置结果了。
1.template <typename F, typename... Args>
F代表函数类型 ...Args函数的参数
2.std::invoke_result_t<F, Args...>类型萃取工具
用来获取调用某个函数对象
F
以参数Args...
所产生的返回类型。让编译器自己推导函数的返回类型3.auto Post(...) -> std::future<std::invoke_result_t<F, Args...>>尾置返回类型语法
它是函数返回类型的一种写法,主要用于模板函数中返回类型依赖参数类型的情况。
1.auto让编译器先跳过函数的返回类型 先去处理函数的参数
2.推导出函数参数F Args...的类型
3.推导完函数参数的类型 才能推导出函数的返回值类型
为什么不能写成这样?
std::future<std::invoke_result_t<F, Args...>> Post(F&& f, Args&&... args);
因为编译器解析函数声明时是从左到右的,获取返回值的类型std::future<std::invoke_result_t<F, Args...>> 但F Args..的类型必须等参数传入后,才能确认。
编译器处理函数模板时是按“返回值 → 函数名 → 参数”的顺序进行的,你不能在返回值里使用还没推导出的模板参数类型,这就是为什么需要尾置返回类型的根本原因。
4.std::packaged_task<>异步任务封装器
std::packaged_task 是一个 把函数和返回值绑定起来的包装器,可以让函数的执行结果通过 std::future 异步获取。
它是 C++ 标准库中为了解决“函数执行完以后怎么拿返回值”的问题而设计的组件。
eg.使用方法
int add(int a, int b) { return a + b; } //1.用 packaged_task+bind 包装它函数 std::packaged_task<int()> task(std::bind(add, 2, 3)); //2.获取关联的future std::future<int> result = task.get_future(); //3.异步执行这个任务(线程、线程池、手动调用都可以) std::thread t(std::move(task)); // 线程里执行 task() t.join(); //4.fet()获取返回值 std::cout << result.get();
和promise最大的区别是,不需要手动设置set_value的值,等bind绑定的函数执行完 自动把函数的返回值填入,之后get()就可以获取到函数的返回值。
对比点 std::packaged_task
std::promise
是否自动设置 future 的值? ✅ 是的,执行函数后自动设置 ❌ 需要你手动调用 .set_value()
谁负责填充结果? 函数执行完后自动填充 你手动调用 .set_value()
是否必须绑定函数? ✅ 是的 ❌ 否,传的是值 packaged_task 最适合线程池,因为线程池就是“执行任务然后获得返回值”,它天然对接任务队列。
“执行一个函数并得到它的返回值” —— 用 packaged_task;
promise 更多用于线程间通信,或者异步控制信号(比如网络回调、I/O完成通知等)。
“告诉另一个线程一个值” —— 用 promise。
2.任务队列(双)
和单队列不同的地方是,Pop()工作线程获取任务时 如果没有任务,不会阻塞在Pop()而是会进行Swap()交换队列(生产者队列有数据或者非阻塞)
if(_con_queue.empty()&&SwapQueue()==0)
先判断消费者队列是否为空,不为空就不会继续后面的判断了,也就不会进行交互队列。
只有消费者队列为空才会进行交换。SwapQueue()会返回交换后的队列有多少任务 ==0说明队列为空(但生产者队列有数据才会交换 没有数据就会阻塞,除非要退出 会设置为非阻塞)
,所以交换完队列还为空就需要返回false进行退出。
交换时 处理消费者需要加锁,生产者也需要加锁。
等生产者队列有数据再进行交换,或者退出设为非阻塞