魔门塔的面试题: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;
}