中科合迅 软件开发-C++ 二面 + HR面

1、C++ 内存模型里 happens-before 是什么

答案:happens-before 可以理解成多线程程序里“先发生关系”的一种保证。如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,并且 A 的执行顺序排在 B 之前。

它不是单纯代码书写顺序,而是语言层面对跨线程可见性和有序性的定义。比如同一个线程内,前面的语句天然 happens-before 后面的语句;再比如一个线程执行 mutex unlock,另一个线程随后成功 lock 同一个互斥锁,那么解锁前的写入对加锁后的读取可见。

面试里这个点经常和数据竞争一起问。如果两个线程对同一个变量并发访问,且至少一个是写,并且它们之间没有 happens-before 关系,那就是 data race,程序行为未定义。

代码:

#include <iostream>
#include <thread>
#include <atomic>
using namespace std;

atomic<bool> ready(false);
int data = 0;

void producer() {
    data = 42;
    ready.store(true, memory_order_release);
}

void consumer() {
    while (!ready.load(memory_order_acquire)) {}
    cout << data << endl; // 这里能看到 producer 对 data 的写入
}

int main() {
    thread t1(producer);
    thread t2(consumer);
    t1.join();
    t2.join();
}

2、memory_order_relaxedacquirereleaseseq_cst 的区别

答案:这是 std::atomic 里最核心的一组内存序问题。

memory_order_relaxed 只保证原子性,不保证线程之间的可见顺序。适合单纯计数这类对顺序不敏感的场景。memory_order_release 一般用于写端,表示这个操作前的普通写不能被重排到它后面。memory_order_acquire 一般用于读端,表示这个操作后的普通读写不能被重排到它前面。当一个线程的 release-store 和另一个线程的 acquire-load 读到同一个值时,就建立了同步关系。memory_order_seq_cst 是最强的顺序一致性,最容易理解,也通常代价更高。

代码:

#include <atomic>
#include <thread>
#include <iostream>
using namespace std;

atomic<int> flag{0};
int x = 0;

void write_thread() {
    x = 100;
    flag.store(1, memory_order_release);
}

void read_thread() {
    while (flag.load(memory_order_acquire) != 1) {}
    cout << x << endl;
}

int main() {
    thread t1(write_thread);
    thread t2(read_thread);
    t1.join();
    t2.join();
}

3、什么是伪共享,怎么解决

答案:伪共享指的是多个线程虽然访问的是不同变量,但这些变量恰好落在同一个 cache line 上。由于 CPU cache line 是一致性协议的基本单位,一个线程修改其中一个变量,会导致其他核心上的同一 cache line 失效,于是产生大量 cache bounce,性能明显下降。

它和真正的共享不一样,真正共享是多个线程访问同一个变量;伪共享是访问不同变量,但由于物理布局太近,硬件层面被当成同一块缓存处理。

常见优化方式有两种:一是做 cache line padding,把高频写变量隔开;二是调整数据结构布局,把不同线程写入的数据拆散,尽量避免落在同一缓存行。

代码:

#include <atomic>
#include <iostream>
using namespace std;

struct alignas(64) Counter {
    atomic<long long> value{0};
};

Counter c1;
Counter c2;

int main() {
    cout << sizeof(Counter) << endl;
}

4、什么是 ABA 问题,CAS 为什么会有这个问题

答案:CAS 只比较“当前值是否等于预期值”,如果相等就更新。ABA 问题的关键在于:某个位置原来是 A,后来被改成 B,又改回 A。此时另一个线程做 CAS 时会发现值还是 A,于是误以为中间什么都没发生。

对于单纯数值场景,这可能不一定有问题;但对于无锁栈、无锁队列这类依赖节点指针状态的结构,中间那次变化可能意味着节点已经被弹出、复用甚至释放,再 CAS 成功就可能踩内存。

常见解决方法:给指针加版本号,比较时同时比较地址和版本;或者用 Hazard Pointer、Epoch Based Reclamation 这类安全回收机制,避免节点过早复用。

5、为什么无锁结构里内存回收比加锁结构更难

答案:因为加锁结构下,谁持有锁谁操作,临界区边界清晰,节点什么时候没人用了相对容易判断。但无锁结构里,一个线程看到某个节点后,可能还没来得及访问完,另一个线程已经把节点从数据结构里摘掉并释放了。这样前一个线程再去访问,就是悬空指针。

所以无锁结构真正难的往往不是 CAS 本身,而是“节点什么时候能安全回收”。这也是为什么很多人能写出看起来能跑的 lock-free 栈,但很难写出真正可在线上长期稳定跑的版本。

6、shared_ptr 是线程安全的吗

答案:这个问题很容易被答偏。shared_ptr引用计数增加和减少通常是线程安全的,所以多个线程分别持有同一个 shared_ptr 的拷贝,一般不会因为计数本身出错。但这不代表 shared_ptr 管理的对象天然线程安全。对象里的成员如何读写,还是要靠业务自己同步。另外,同一个 shared_ptr 实例如果被多个线程同时读写,也不是完全无条件安全的,通常还是应该避免多个线程无保护地同时修改同一个 shared_ptr 对象本身。

7、说一下 enable_shared_from_this 的原理和使用坑点

答案:它的作用是让对象在内部安全地拿到指向自己的 shared_ptr,而不是手动 shared_ptr(this)。因为如果对象已经被某个 shared_ptr 管理,再在内部用裸指针重新构造一个新的 shared_ptr(this),就会产生两个独立控制块,最后析构两次。

enable_shared_from_this 内部本质上保存了一个指向自身控制块的弱引用,当对象第一次被 shared_ptr 托管时,这个弱引用会被正确初始化。之后调用 shared_from_this(),就能从同一个控制块里构造出新的 shared_ptr

坑点主要有两个:第一,对象必须已经被 shared_ptr 托管,否则调用 shared_from_this()会出问题;第二,不能在构造函数里贸然调用,因为这时控制块往

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论

相关推荐

评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务