基础部分
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;
补充
- 僵尸线程:已经结束运行的线程,但其资源未被回收。不占用 CPU,但仍占用少量内核资源。父线程未回收资源(jion)。会导致资源泄漏,如果大量僵尸线程积累,会消耗内核线程表,最终无法创建新线程。
- TLS补充:实现方式:编译器/操作系统为每个线程维护一个 TLS 段(存放 TLS 变量),访问时根据线程 ID 定位。创建时分配对于TLS存储空间,线程退出释放TLS变量。TLS每个线程独享,全局变量所有线程共享。访问TLS开销接近访问全局变量,比加锁访问共享变量要低。
自旋锁与互斥锁的区别
都是互斥类型的锁
自旋锁:
核心区别在于线程等待锁的策略和开销:
- 对于互斥锁如果线程拿不到锁,他就会阻塞休眠
- 对于自旋锁如果线程拿不到锁,他就会在一个循环里不停尝试获取(忙等待)
- 互斥锁的开销来自于两次上下文切换(睡眠一次,唤醒一次)
- 自旋锁开销来自cpu空转的时间
适应场景
- 如果临界执行时间长,或者有IO操作,一定要用互斥锁,否则空转浪费大量cpu
- 如果临界执行时间短,就是简单指令,那么自旋锁避免上下文切换带来的收益就会大于空转的损耗,性能更高。
如何选择:优先使用std::mutex,如果明确知道(性能测试工具)互斥锁是性能瓶颈,才考虑使用自旋锁优化。
lambda表达式与bind的区别
- 可读性:lambda直接表示函数调用逻辑、参数传递清晰;bind依赖占位符,映射参数位置,需要查原函数声明
- 重载支持:lambda通过常规函数调用自动匹配重载版本;bind无法自动区分重载函数,需要显示转换函数指针类型
优先使用lambda;更易读、表达力强、效率高。
1051

被折叠的 条评论
为什么被折叠?



