魔门塔的面试题:5 个线程并发往 100 个元素的数组里写,要求每个元素恰好被写一次、写入自己的线程号,最后还要输出每个线程各写了多少次。
三个关键点
1. 原子发号,号全局唯一
auto i{next_index.fetch_add(1, std::memory_order_relaxed)}; // 取号
fetch_add 是原子的读-改-写:读旧值、+1、写回,一气呵成。返回的下标不可能有两个线程拿到同一个 → 每一格只有一个人写,天然无竞争。
2. 各写各格
nums[i] = tid; // 写自己那一格
++write_count[tid]; // 自己写了几个,各记各的账
nums[i] = tid 是普通写,但 i 唯一,写的是不同格子,互不干扰——这就是"无锁"的底气。每个线程维护自己的 write_count,各写各的计数器,最后汇总就是总数。
3. relaxed 就够
std::atomic<int> next_index{0};
// ...
next_index.fetch_add(1, std::memory_order_relaxed);
下标唯一性靠 fetch_add 的原子性本身;主线程读全部结果靠 join() 提供 happens-before。这里没有跨线程"先写后读"的依赖,所以用不上 acquire/release。
可能的问题
- 什么是
std::memory_order_relaxed? 最弱的原子内存序:只保证原子操作本身的原子性(不撕裂、不重复),不建立任何跨线程的读写顺序。 - 为什么 relaxed 够? 唯一性靠原子操作,可见性靠 join,两者都不需要更强的内存序。
- 把
fetch_add换成++next_index呢? 非原子,两个线程可能同时拿到同一个 i、重复写同一格——数据竞争。 - 用 mutex 锁下标呢? 能解,但"原子取号"更轻,一个
fetch_add就搞定。
记住它
像银行取号机:每个线程取一个号,拿号去写自己的格子,谁也不抢谁的。100 个格子各写一次、total 恒为 100;每个线程写几个全看调度,不固定。
完整代码
#include <array>
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
int main() {
constexpr auto kNum{100}; // 100 个格子
constexpr auto kThreadNum{5}; // 5 个线程
std::array<int, kNum> nums{};
std::atomic<int> next_index{0}; // 发号器
std::array<int, kThreadNum> write_count{};
write_count.fill(0);
std::vector<std::thread> workers;
workers.reserve(kThreadNum);
for (auto tid{0}; tid < kThreadNum; ++tid) {
workers.emplace_back([tid, &nums, &next_index, &write_count]() {
while (true) {
auto i{next_index.fetch_add(1, std::memory_order_relaxed)}; // 取号
if (i >= kNum) { // 号发完了,退场
break;
}
nums[i] = tid; // 写自己那一格
++write_count[tid]; // 记账
}
});
}
for (auto& th : workers) {
th.join();
}
auto total{0};
for (auto tid{0}; tid < kThreadNum; ++tid) {
std::cout << "thread " << tid << " wrote " << write_count[tid] << "\n";
total += write_count[tid];
}
std::cout << "total writes = " << total << "\n";
return 0;
}