烤面筋 Day 01

烤面筋 Day 01


原子操作与内存模型:


1. 原子操作是什么?Atomic 跟原子操作的关系是什么?

答:原子操作就是要么不执行,要么全部执行成功,且中途不能被中断的操作。Atomic 就是 C++ 11 提供的标准库模板类,是原子操作在语言层面的封装和工具。

2. Atomic 底层是怎么实现的?

答:底层主要依赖硬件层面处理器提供的原子指令,在 x86 架构下,通常会使用带有 LOCK 前缀的指令,其能锁定缓存行,确保执行执行期间不会被中断。

编译器层面也会根据不同的 CPU 架构,将 atomic 操作映射为相应的机器指令,根据指定的内存序,插入必要的 Memory Barrier ,防止指令重排。

3. 能简单讲讲 C++ 的内存序(Memory Order)吗?

内存序的作用是约束编译器和 CPU 的指令重排优化,C++11 定义了 6 种内存序,但常用的是 memory_order_relaxed 和 获取/释放语义和序列一致性。

宽松顺序:只保证当前操作的原子性,不保证任何顺序(没有同步关系)。

释放(release)语义:持有 release 的线程是发布者,,逻辑是 “做完手头的事情了,现在发布表示数据准备好了”

获取(acquire)语义:持有 acquire 的线程是获取者,逻辑是 “看到发布的通知了,说明我可以放心去用数据了”

序列一致性:atomic 的默认参数,要求所有线程看到的执行顺序都是一模一样的,就像所有操作都排在一个全局队列里依次执行。

  • 那为什么不能全用默认的 seq_cst 序列一致性的内存序?

高性能场景下(如无锁队列、底层驱动),默认的强顺序会导致大量的内存屏障指令,强制刷新 CPU 缓存,这会极大限制现代多核处理器的并行能力。

4. CAS 指令是什么?

答:CAS 是实现原子操作的核心指令,包含三个操作数:

1
2
3
1. 内存地址 V:要读取的变量位置。
2. 旧的预期值 A:我们认为该变量当前应该是什么值。
3. 准备写入的新值 B:如果检查通过,要更新成的值。

只有内存地址的值还是我之前读到的那个预期值,我才把它修改成新值。否则说明别人改过它,那我就放弃这次修改。整个过程是原子性

5.CAS 的优缺点是什么?

答:优点就是无锁,避免了传统互斥锁导致的线程上下文切换、挂起和恢复,性能开销更小。

缺点是有自旋开销,如果在高竞争环境下,CAS 一直失败,会导致 CPU 一直空转,浪费资源。同时还存在 ABA 问题。

6. ABA 问题是什么?如何解决这个问题?

就是一个线程将数值 A 改成了 B,随后另一个线程又将 B 改回了 A,此时第三个线程进行检查,发现值依然是 A,认为没有改变,从而误操作。

目前解决的主流办法是版本号,实现方法就是记录值的同时维护一个版本号,每次更新的时候,不仅更新值,还让版本号 +1。这样遇到 ABA 问题的时候版本号不同,就知道值已经发生改变了。

7. C++ atomic 是否自带版本号机制?

不自带。标准的 std::atomic 只是对类型 T 的原子封装。如果 T 是一个指针,仍然会存在 ABA 问题。

如果需要解决这个问题的话,开发者通常需要配合 std::atomic<std::pair<T, int>> 或者一些带有 Tag 的指针技术。

8. Volatile 关键字的作用是什么?能解决线程安全吗?

主要的作用是禁止编译器优化:告诉编译器,这个变量的值可能随时被外部(如硬件中断、另一个进程)修改,因此每次使用它都必须从内存中读取,而不能使用寄存器里的缓存。

不能,因为他不保证原子性,同时不禁止指令重排,只针对编译器的,但不提供针对 CPU 的内存屏障,无法解决多核环境下的可见性和有序性问题。

9. 手撕一下:写一下 CAS 实现的无锁队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <atomic>

template <typename T>
class LockFreeQueue {
public:
void Enqueue(const T& val) {
Node* new_node = Node(val);
Node* old_tail = tail.load();

while(true) {
old_tail = tail.load();
Node* next = old_tail->next.load();

if(old_tail == tail.load()) {
if(next == nullptr) {
if(old_tail->next.compare_exchange_strong(next, new_node)) {
tail.compare_exchange_weak(old_tail, new_node);
return;
}
}
else tail.compare_exchange_strong(old_tail, next);
}
}
}
private:
struct Node {
T data;
std::atomic<Node*> next;
Node(T val) : data(val), next(nullptr) {}
};
std::atomic<Node*> head;
std::atomic<Node*> tail;
};

int main(int argc, char* argv[]) {}

多线程与锁


1. 多线程最常见的问题是什么?

主要是三个问题:

  • 线程安全问题:多个线程同时读写同一个块内存,导致结果不可预期

  • 活跃性问题:包括死锁,活锁和饥饿,其中死锁是最致命的,会导致程序完全卡死

  • 性能开销:过渡的上下文切换和锁竞争导致性能下降

2. 死锁怎么避免?

避免死锁的核心逻辑是:防止循环等待链的形成。常用的解决办法如下:

  • 固定加锁顺序:规定所有的线程必须按照相同的顺序获取锁(先拿 A 锁,再拿 B 锁)
  • 尝试锁(Try-lock):使用 std::unique_locktry_lock,如果拿不到锁就立即释放已占用的所并且回退
  • 使用高级抽象:使用其他无锁的机制(比如消息传递等等)来替代底层锁

3. 哪些设计方式/技巧能破坏死锁的 4 个条件?

互斥性:资源一次只能被一个线程占用。这个是无法破坏(锁的本意)。

占用且等待:线程持有一个资源的同时,还在等待另一个资源。采用一次性申请的方式解决: 线程启动前一次性申请所有需要的锁;或者在无法获取下一个锁时,释放已占有的所有锁。

不可剥夺:资源不能被抢占,只能由持有者主动释放。采用抢占机制的方式解决:如果线程申请新锁失败,主动释放手中资源,过段时间再重试

循环等待:存在一个线程等待环路:T1 等 T2,T2 等 T1。采用资源线性排序的方式解决: 给所有锁分配唯一序号,强制规定必须按序号从小到大(或从大到小)获取锁。

4. 实际开发中,你会如何实现避免死锁的发生?

我会使用 std::scoped_lock 一次性添加多个锁利用其内部的机制来避免

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <mutex>
#include <thread>

class Account {
public:
int balance = 1000;
std::mutex mtx; // 每个账户自带一把锁
};

void transfer(Account& from, Account& to, int amount) {
// 关键点:std::scoped_lock 同时接收多个 mutex
// 它内部使用了一种免死锁算法(通常是逐个尝试且不持有等待)
// 无论你传入参数的顺序如何,它都能保证不会发生死锁
std::scoped_lock lock(from.mtx, to.mtx);

if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
std::cout << "转账成功: " << amount << std::endl;
}
}

int main() {
Account alice, bob;
// 线程 1:从 Alice 转账给 Bob
std::thread t1(transfer, std::ref(alice), std::ref(bob), 100);
// 线程 2:从 Bob 转账给 Alice (如果用普通 lock 会因顺序相反导致死锁)
std::thread t2(transfer, std::ref(bob), std::ref(alice), 200);
t1.join();
t2.join();

return 0;
}

其内部的机制

  • 避免循环等待:内部并不简单地按顺序调用 lock(),而是采用了一种 “重试机制”

​ 1. 它会尝试锁住第一个互斥量。

​ 2. 接着尝试 try_lock 后面的互斥量。

​ 3. 如果中间任何一个锁失败了,它会立即释放已经持有的所有锁,然后重新开始。

  • 原子性加锁:保证了所有传入的互斥量要么全部锁住,要么一个都不锁。

烤面筋 Day 01
https://dxblacksmith.github.io/2026/01/13/烤面筋_Day01/
作者
DxBlackSmith
发布于
2026年1月13日
许可协议