C++多线程高频面试清单

基础部分

1、进程与线程的区别

  • 进程是操作系统资源分配的最小单位,线程是CPU调度的最小单位
  • 进程之间是隔离,线程共享进程的地址空间
  • 创建/切换进程开销大,线程开销小。(上下文切换需要从用户态写入内核态,因此开销较大)

2、线程进程的通讯方式

  • 进程:管道,消息队列、共享内存
  • 线程:信号量、条件变量、原子变量

3、pthread与std::thread的区别

  • pthread是c的库函数,跨平台差异大,顶层是对Linux内核线程的封装,需要手动调用
  • std::thread来自于C++11标准库,是对线程的C++封装,内部通常基于pthread实现,接口是面向对象,具有跨平台能力。本质上还是由pthread实现的,支持RALL、lambda。
  • 简单来说pthread手动挡,std::thread是自动挡

生命周期管理

4、join()和detach()的区别

  • detach分离线程,让系统后台回收资源(不可再jion)
  • jion等待线程结束并回收资源(阻塞),忘记jion会产生僵尸线程,重复jion导致程序异常

5、如果忘记jion/detach会怎样

  • pthread:线程会变成僵尸线程,资源泄露。
  • std::thread:析构未jion/detach会调用std::terminte,程序直接崩溃。

6、线程结束的方式有哪些

  • 线程函数,return
  • 调用pthread_exit /  std::exit

同步与锁

7、C++提供哪些线程同步工具

  • std::mutex、std::lock_guard、std::unique_lock、std::condition_variable、std::future/primise、std::atomic

8、lock_gurad和unique_lock区别

  • lock_guard:RALL简单封装,自动加锁解锁,不能unlock
  • unique_lock:更灵活,可unlock/lock,多次加锁,支持延迟加锁

9、条件变量的作用

  • 用于线程间的通信,生产者-消费者场景
  • 线程等待条件满足时阻塞,被其他线程唤醒

10、避免死锁

  • 固定加锁顺序
  • 使用std::lock
  • 尽量减少颗粒度

内存模型与原子操作

11、C++11为什么引入内存模型

不同平台CPU/编译优化不同,C++11统一内存可见性语义,保证跨平台

内存模型:定义多线程程序中不同线程访问共享变量时的行为规则,主要解决下面问题

  • 可见性:一个线程共享变量的修改什么时候对其其他线程可见
  • 有序性:指令从排导致执行顺序和代码顺序不同
  • 原子性:多线程对同一变量操作

12、volatile能保证线程安全?

volatile是c++11引入的关键字,他保证变量不被优化,不保证原子性。

13、std::atomic是怎么实现的

是基于CPU原子操作,提供原子读写,避免数据竞争

14、内存序有哪些

  • memory_order_relaxed:只保证原子性,不保证顺序
  • memory_order_acquire/release:保证操作前后顺序
  • memory_order_seq_cst:顺序一致性,最严格

高级并发

15、线程池的原理

  • 任务队列+固定线程数量
  • 工作线程不断从队列中取任务,避免频繁创建/销毁。
  • 通常使用条件变量std::condition_bariable实现阻塞队列

16、为什么需要线程池

  • 减少线程创建/销毁。
  • 实现并发,并控制并发线程数量(过少与过多都不行,过少会降低并发量,过多会增大压力)
  • 适合短小任务和高并发场景

17、std::async和std::thread的区别

  • std::thread:立即创建线程并运行
  • std::async:可能创建新线程,也可能复用线程池,返回future,更适合任务并发。

18、package_task的作用

把可调用对象(函数、lambda)包装成异步任务,返回future,常用于线程池并发

19、std::future 和std::promise区别

  • future:获取异步结果
  • promise:设计异步结果
  • 两者配合用来传递线程间的数据

常见陷阱

20、什么是数据竞争

  • 多个线程访问同一个变量
  • 结果不可预测

21、为什么多线程程序比单线程更慢

  • 上下文开销过大
  • 锁竞争导致阻塞
  • 缓存一致性开销

22、线程函数传递局部变量的坑

  • 如果传递局部变量的引用或指针,线程可能在变量销毁后才运行,悬空引用
  • 解决:传值,或保证生命周期

23、线程局部存储TLS实现

每个线程拥有自己独立的一份变量副本,不同线程访问相同TLS变量互不干扰。

  • pthread:pthread_key_create + pthread_setspecific
  • C++11:thread_local 关键字,简单高效。thread_local int counter = 0;

补充

  1. 僵尸线程:已经结束运行的线程,但其资源未被回收。不占用 CPU,但仍占用少量内核资源。父线程未回收资源(jion)。会导致资源泄漏,如果大量僵尸线程积累,会消耗内核线程表,最终无法创建新线程。
  2. TLS补充:实现方式:编译器/操作系统为每个线程维护一个 TLS 段(存放 TLS 变量),访问时根据线程 ID 定位。创建时分配对于TLS存储空间,线程退出释放TLS变量。TLS每个线程独享,全局变量所有线程共享。访问TLS开销接近访问全局变量,比加锁访问共享变量要低。

自旋锁与互斥锁的区别

都是互斥类型的锁

自旋锁

核心区别在于线程等待锁的策略和开销:

  • 对于互斥锁如果线程拿不到锁,他就会阻塞休眠
  • 对于自旋锁如果线程拿不到锁,他就会在一个循环里不停尝试获取(忙等待)
  • 互斥锁的开销来自于两次上下文切换(睡眠一次,唤醒一次)
  • 自旋锁开销来自cpu空转的时间

适应场景

  1. 如果临界执行时间长,或者有IO操作,一定要用互斥锁,否则空转浪费大量cpu
  2. 如果临界执行时间短,就是简单指令,那么自旋锁避免上下文切换带来的收益就会大于空转的损耗,性能更高。

如何选择:优先使用std::mutex,如果明确知道(性能测试工具)互斥锁是性能瓶颈,才考虑使用自旋锁优化。

lambda表达式与bind的区别

  1. 可读性:lambda直接表示函数调用逻辑、参数传递清晰;bind依赖占位符,映射参数位置,需要查原函数声明
  2. 重载支持:lambda通过常规函数调用自动匹配重载版本;bind无法自动区分重载函数,需要显示转换函数指针类型

优先使用lambda;更易读、表达力强、效率高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值