C++线程池实战

本文作为C++线程池相关内容,读者需要有一定的C++知识储备及对并发编程的内容有所掌握。

通过学习,不仅可以掌握线程池的部分,同时也可对C++基础知识进行复习。

不仅仅是对线程池的讲解,同时会对线程池的衍生使用方法进行讲解实战。在项目出现的一些函数或者方法会做介绍,方便读者学习。

接下来进入正题:

一、线程池

1.1 什么是线程池?


        线程池是 C++ 并发编程中管理线程资源的核心技术,它预先创建一组线程(称为 “工作线程”),这些线程不会在执行完单个任务后销毁,而是回到线程池等待下一个任务,从而避免反复创建 / 销毁线程的高昂开销(线程创建需分配栈空间、内核资源,销毁需回收资源,频繁操作会显著消耗系统性能)。

1.2为什么需要线程池?

在 C++ 开发中,直接使用std::thread创建线程存在以下问题:

  • 资源浪费:频繁创建线程会导致内核态与用户态切换频繁,占用大量内存(每个线程默认栈大小通常为 1-8MB)。
  • 并发失控:无限制创建线程可能导致 “线程爆炸”,引发 CPU 上下文切换过载、系统响应变慢甚至崩溃。
  • 编程复杂:手动管理线程生命周期(创建、同步、销毁)会增加代码复杂度,易出现死锁、数据竞争等问题。

线程池通过 “预创建线程 + 任务队列” 的模式,完美解决上述问题,是高并发场景(如服务器、批量任务处理)的必备工具。

1.3线程池的核心组件与工作原理

模块名称

主要职责

线程池(ThreadPool)

管理整个系统,创建线程、维护任务队列、调度执行、关闭资源。

任务队列(TaskQueue)

用于存放待执行的任务,通常采用安全容器(如std::queue+互斥锁保护)。

工作线程(Worker)

实际执行任务的线程,从任务队列中取任务并执行。

同步机制(锁+条件变量)

保证线程之间访问共享队列时不出现竞争;条件变量用于任务到来时唤醒线程。

工作流程:

①提交任务
主线程调用 submit() 或类似接口,将任务(一般是函数或可调用对象)放入任务队列中。
同时通过 condition_variable.notify_one() 唤醒一个等待中的工作线程。

②工作线程等待与唤醒
每个工作线程在启动后会进入一个循环:

  • 使用 condition_variable.wait() 阻塞,等待任务队列中出现新任务;
  • 一旦有任务到来,条件变量被唤醒,线程获得锁,从队列中取出任务。

③任务执行阶段
线程取到任务后释放锁(避免阻塞其他线程取任务),执行该任务。

  • 若任务抛出异常,一般需要在线程内捕获,以防止线程异常退出。
  • 任务执行完毕后,线程继续回到等待状态。

④线程池关闭阶段
当主程序调用 shutdown() 或销毁线程池对象时:

  • 设置停止标志 stop_flag = true
  • 唤醒所有等待的线程;
  • 每个线程检测到停止标志后退出循环;
  • 主线程 join() 所有子线程,确保安全退出。

1.4代码展示如下所示

代码前瞻:

ThreadPool类的数据结构:

使用queue作为任务队列:线程池的核心是按任务提交的顺序执行(先进先出,FIFO),因为

大多数场景下,任务的提交顺序与执行优先级无关,先提交的任务应优先被工作线程处理。

这种调度方式最简单、直观,能避免复杂的优先级管理,减少线程池的实现复杂度。而queue刚好满足这种要求,且它的接口简单,很适合线程池的核心操作。

vector作为工作线程,里面的数据类型为是std::thread。

stop_flag:标记线程池运行状态。也是实现析构的核心数据结构

从上至下依次说明:

        首先是线程池Thread Pool的构造函数,在这里实现线程池。利用emplace_back在vector尾部构造线程,它的作用是减少内存开销(直接构造而不用复制数据),每调用一次就会创建并启动一个线程,并且在这个for循环中,线程执行不会影响线程创建。主线程创建线程 1 后,不等线程 1 进入阻塞,就会继续执行循环创建线程 2、3…… 直到所有线程创建完成。因为要将任务队列中的任务取出来,所以我们需要上锁,防止别的线程过来横插一脚对数据进行修改。接下来就是线程等待被唤醒,这里的逻辑是如果线程池不在运行或者线程池不为空,则线程被唤醒,包括下面的那行代码,是while(true)能够实现退出的关键:如果线程池运行标志stop_flag为真同时任务队列为空,则退出循环(直接退出当前的 while(true) 循环,并且会退出整个线程的执行函数(lambda 表达式)。)总结:该函数就是创建线程池并提取任务队列中的成员去执行它。

submit函数这里使用了万能引用和尾置返回类型。它作为类的外部接口,将需执行的任务打包后传入tasks任务队列中,所有线程均有可能获得执行这个任务的机会,所以在他们之前这个任务是共享的。这里的res(std::future对象)的作用就是在异步执行的任务和主线程之间建立可靠的通信渠道,确保结果不丢失、执行状态可感知、异常可处理。没有它,线程池只能 “盲目执行” 任务,无法满足实际开发中对任务结果和执行状态的需求。将任务添加到任务队列后,唤醒一个线程来执行它。

看懂了线程池的构造函数是如何实现退出后,对析构函数也能轻易的掌握。修改stop_flag标记实现退出所有的线程任务。最后一定要join掉所有线程。

输出结果如下所示:

可以看到两个执行结果不一样:一个是乱序的一个顺序的,其实如果多次执行结果大概率也是不一致的。为什么会导致输出结果不同呢?多个线程同时操作一个输出流,导致并发写冲突:比如线程A要输出Task 1 :done,线程B要输出Task 2 :done,由于上述原因,输出结果可能是这样的:Task 1: Task 2: done done。解决的方法很简单,就是在输出段加一个即可实现顺序输出。

1.5代码的补充

上述代码中有一些常见的问题 ,以下作为补充介绍

①为什么要捕获this:

如果不捕获this,lambda 里无法直接用queue_mutex或tasks—— 因为这些是类的成员,不是 lambda 的局部变量。

为什么要无限循环?----这个是要点

因为线程池的核心是 “复用线程”:一个线程执行完一个任务后,不能销毁,要继续等下一个任务,所以用循环让线程 “一直活着” 并待机。while循环是保证每个线程都保持在线状态,不会被主动销毁。

③为什么要使用cv.wait()?

这是最关键的部分,避免线程空循环浪费CPU

它可以阻止线程在while(true)中疯狂循环,不满足或条件则解除当前锁,同时休眠线程,等待被唤醒;否则的话 每次循环都加锁→判断任务队列是否为空→解锁→再循环…… 即使队列空,也会反复加解锁,占用大量 CPU(空轮询) 。

具体流程:

具体流程:

  1. 先检查 “谓词”(this->stop || !this->tasks.empty()):
    • 若满足条件(比如任务队列非空):不休眠,直接往下执行(取任务)。
    • 若不满足(队列空且没停止):释放锁→让线程休眠→直到被cv.notify_one()/cv.notify_all()唤醒。

      2.唤醒后:重新加锁→再次检查谓词→满足则继续执行,不满足则再次休眠。

在第一循环时,第一个线程wait阻塞,此时for循环开始执行第二次循环,开始构造第二个线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值