【C++】面试:多线程并发

中高级面试必考模块,涵盖多线程、锁、C++11 新特性、模板、编译链接,偏向工程实战与底层原理,综合区分技术深度。


1. C++ 线程创建与线程函数

1.1 线程创建方式(std::thread)

C++11 提供标准线程库 std::thread,跨平台,替代系统原生线程 API。

1.2 四种可作为线程入口的函数

  1. 普通全局函数
  2. 类静态成员函数
  3. lambda 表达式
  4. 仿函数 (重载 operator())

1.3 核心接口与规则

  • thread t(入口函数, 参数):创建并自动启动线程。
  • join():阻塞主线程,等待子线程执行完毕,回收线程资源。
  • detach():分离线程,线程后台运行,主线程不再等待,由系统自动回收资源。
  • 禁止:线程对象生命周期结束前,未 join/detach,程序崩溃。

高频坑点

线程传参默认值拷贝 ;如需传递引用,使用 std::ref/std::cref


2. 互斥锁 mutex、死锁成因与避免

2.1 std::mutex 互斥锁

作用:保护共享资源,保证同一时刻仅一个线程访问临界区,解决数据竞争。

  • lock():加锁,阻塞等待
  • unlock():解锁
  • std::lock_guard:RAII 风格自动锁,构造加锁、析构解锁,避免漏解锁。

2.2 死锁四大必要条件(必背)

  1. 互斥条件:资源同一时刻仅一个线程持有。
  2. 请求与保持:线程持有已有资源,又请求新资源。
  3. 不可剥夺:资源不能被强行抢占,只能持有者主动释放。
  4. 循环等待:线程间形成环形资源等待链。

2.3 死锁解决方案

  1. 破坏循环等待:统一锁的加锁顺序
  2. 破坏请求保持:一次性申请所有需要的锁。
  3. 使用 std::try_lock:尝试加锁,失败则主动释放已有资源。
  4. 减少嵌套锁,尽量降低锁粒度。

3. 条件变量 condition_variable

3.1 作用

实现线程间同步与等待唤醒,常搭配互斥锁使用,用于生产者-消费者模型。

3.2 核心接口

  • wait(lock):解锁并阻塞当前线程,等待被唤醒;唤醒后重新加锁。
  • notify_one():唤醒一个等待中的线程。
  • notify_all()唤醒所有等待线程。

3.3 标准使用范式

  1. 线程先持有互斥锁。
  2. 调用 wait 进入等待,释放锁。
  3. 其他线程满足条件后执行 notify 唤醒。
  4. 被唤醒线程重新获取锁,继续执行。

补充

wait 存在虚假唤醒 ,必须搭配 while 循环判断条件,不能用 if。


4. 原子变量 atomic 作用

4.1 定义

std::atomic C++11 原子类型,无锁并发方案。

4.2 核心作用

  1. 保证变量读写是原子操作,不会被线程打断。
  2. 不使用互斥锁,规避锁的开销,性能更高。
  3. 解决多线程下简单数值竞争问题(计数、标志位)。

4.3 适用与局限

  • 适合:计数器、状态标记、简单加减运算。
  • 局限 :仅保障单个变量原子性,复杂业务逻辑仍需锁。

对比

atomic 轻量高效;mutex 功能强大,适用于复杂临界区。


5. 锁的性能对比(互斥锁 / 自旋锁)

5.1 互斥锁 mutex

  • 原理:加锁失败时,线程休眠,让出 CPU,进入等待队列。
  • 开销:线程切换、上下文切换开销大。
  • 适用场景:临界区执行时间长、锁等待时间久。

5.2 自旋锁 spin_lock

  • 原理:加锁失败,线程循环轮询,不放弃 CPU。
  • 开销:无上下文切换,CPU 空转消耗。
  • 适用场景:临界区极短,预计很快获取锁。

5.3 选型总结

  1. 代码执行快、竞争轻微 → 自旋锁。
  2. 代码执行慢、长时间等待 → 互斥锁。
  3. C++ 标准库无原生自旋锁,可使用 std::mutex + 自定义自旋、或系统 API 实现。

6. C++11 lambda 表达式详解

6.1 基础语法

[捕获列表](参数列表) mutable -> 返回值类型 { 函数体 }

6.2 捕获方式(必考)

  1. []:不捕获任何变量。
  2. [=]:值捕获,拷贝外部所有变量。
  3. [&]:引用捕获,引用外部所有变量。
  4. [var]:仅值捕获指定变量。
  5. [&var]:仅引用捕获指定变量。
  6. [this]:类内 lambda,捕获当前对象 this 指针。

6.3 关键字说明

  • mutable:允许修改值捕获的变量(默认只读)。
  • -> 返回值:函数体多分支、返回值不统一时必须显式指定。

6.4 应用场景

线程入口、STL 算法回调、临时简单函数、异步回调。


7. 右值引用与移动语义

7.1 左值 & 右值

  • 左值:有名字、可取地址的变量。
  • 右值:临时值、无名字、不可取地址(字面量、函数返回临时对象)。

7.2 右值引用 &&

语法:类型&&,专门绑定右值,延长临时对象生命周期。

7.3 移动语义 std::move

  1. std::move强制将左值转为右值,本身不移动数据。
  2. 移动构造/移动赋值:盗取临时对象资源,浅拷贝接管堆内存,替代深拷贝。
  3. 价值:避免大对象拷贝,大幅提升性能。

7.4 核心结论

拷贝语义:复制一份新数据;移动语义:转移资源所有权,零拷贝开销。


8. 完美转发 std::forward

8.1 问题背景

模板函数传参时,参数左右值属性会丢失,无法原样转发。

8.2 std::forward 作用

保持参数原有左值/右值属性,实现参数原样转发。

8.3 万能引用 + 完美转发组合

  • 万能引用 T&&:既能接收左值,也能接收右值。
  • 搭配 std::forward<T>(arg):保留值类别,完成完美转发。

区分记忆

  • std::move:左值转右值,一定移动
  • std::forward:原样转发,保留原有属性

9. 模板基础、函数模板与类模板

9.1 模板核心作用

实现代码复用、泛型编程,一套代码适配多种数据类型,编译期实例化。

9.2 函数模板

  1. 语法:template <typename T> 函数定义
  2. 原理:编译器根据实参类型,自动推导 T 并生成对应重载函数。
  3. 特点:类型安全,编译期检查。

9.3 类模板

  1. 语法:template <class T> class 类名
  2. 原理:使用时必须显式指定类型,无法自动推导。
  3. 应用:STL 容器全部基于类模板实现。

9.4 模板特化

  • 全特化:针对某一具体类型单独实现。
  • 偏特化:针对部分参数/类型特征定制实现。

10. 编译链接四大过程(预处理 / 编译 / 汇编 / 链接)

1. 预处理(.c/.cpp → .i)

  • 展开宏、替换文本
  • 处理 #include 头文件、删除注释
  • 处理条件编译 #ifdef / #endif

2. 编译(.i → .s 汇编文件)

  • 语法、语义、类型检查
  • 翻译成汇编代码,优化代码

3. 汇编(.s → .o/.obj 目标文件)

  • 汇编指令转为二进制机器码
  • 生成目标文件,包含代码段、数据段、符号表

4. 链接(多个.o → 可执行文件)

  1. 合并所有目标文件、段表。
  2. 符号解析:找到未定义函数/变量地址。
  3. 地址重定位:修正虚拟地址。
  4. 分为静态链接动态链接

编译错误 vs 链接错��

  • 编译错误:语法错误、头文件缺失、类型不匹配。
  • 链接错误:函数未实现、重复定义、符号未找到。

🔥 本章综合高频追问

  1. :移动构造为什么不常手动写?

    :编译器会在满足条件时合成默认移动构造

  2. :线程 detach 后还能访问局部变量吗?

    :不建议,主线程退出会销毁局部变量,子线程野访问崩溃。

  3. :模板是编译期还是运行期特性?

    编译期,模板会在实例化阶段生成对应代码。


📝 模块总结

本模块是 C++ 进阶分水岭,覆盖多线程并发、C++11 核心新特性、泛型模板、编译原理。面试中用于考察工程能力与语言深度,全部掌握可应对中高级岗位综合面试。