You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++仅用std::atomic共享线程变量为何仍出现数据遗漏?

问题根源与无锁解决方案

嘿,我来帮你捋清楚这个问题——其实你用std::atomic_uint的思路完全没问题,根本不是taken的递增导致数字被跳过,问题出在你用来标记完成状态的vector<bool>上!

为什么会出现“数字被跳过”的假象?

std::vector<bool>是C++标准库的一个特殊实现:它是一个位压缩容器,每个元素只占1位。这意味着当你修改某个元素时,底层需要先读取整个字节(包含8个元素的位),修改目标位,再把字节写回内存。这个过程不是原子的!

当多个线程同时修改同一个字节里的不同位时,就会出现竞态条件:比如线程A要设置第0位,线程B要设置第1位,两者同时读取到同一个初始字节值,各自修改后写回,其中一个线程的修改就会被另一个覆盖。最终主线程看到这些位还是false,误以为对应的数字没被处理,但实际上所有数字都被线程处理过了,只是标记丢了。

无锁解决方案(不用std::mutex)

这里有两个轻量级的方案,都能避免竞态,而且开销远低于 mutex:

方案1:改用std::vector<std::atomic<bool>>

把普通的vector<bool>换成原子布尔类型的容器,这样每个元素的赋值都是原子操作,不会出现位覆盖的问题:

#include <iostream>
#include <chrono>
#include <vector>
#include <algorithm>
#include <thread>
#include <atomic>

int main() {
const uint MAX = 1000;
// 改用原子布尔容器
std::vector<std::atomic<bool>> isPrime(MAX), done(MAX);
// 初始化原子容器(不能用std::fill,手动循环设置)
for (auto& b : done) b = false;
for (auto& b : isPrime) b = false;

std::atomic_uint taken{0}; //shared variable
std::vector<std::thread> threads;
auto start = std::chrono::system_clock::now();

for (uint i = 0; i < 8; ++i) {
threads.emplace_back(
[&](){
bool res;
for (uint tested; (tested = taken.fetch_add(1)) < MAX; ) {
res = true;
for (uint k = 2; k < tested; ++k) {
if (tested % k == 0) {
res = false;
break;
}
}
isPrime[tested] = res;
done[tested] = true;
}
}
);
}

for (auto & t : threads) {
t.join();
}

auto end = std::chrono::system_clock::now();
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

// 统计时需要加载原子值
uint num = 0;
for (const auto& b : isPrime) {
if (b.load()) num++;
}
uint nDone = 0;
for (const auto& b : done) {
if (!b.load()) nDone++;
}

std::cout << "number: " << num << " duration: " << milliseconds.count() << '\n';
std::cout << "not done: " << nDone << '\n';
for (uint i = 0; i < MAX; ++i) {
if (!done[i].load()) {
std::cout << i << ", ";
}
}
std::cout << '\n';
return 0;
}

方案2:改用std::vector<char>或std::vector<uint8_t>

在绝大多数现代CPU上,单字节的读写是原子操作(不会被拆分成多个指令)。我们可以用单字节类型代替位容器,既避免竞态,又比原子布尔有更低的开销:

#include <iostream>
#include <chrono>
#include <vector>
#include <algorithm>
#include <thread>
#include <atomic>

int main() {
const uint MAX = 1000;
// 用char容器代替vector<bool>
std::vector<char> isPrime(MAX, 0), done(MAX, 0);

std::atomic_uint taken{0}; //shared variable
std::vector<std::thread> threads;
auto start = std::chrono::system_clock::now();

for (uint i = 0; i < 8; ++i) {
threads.emplace_back(
[&](){
bool res;
for (uint tested; (tested = taken.fetch_add(1)) < MAX; ) {
res = true;
for (uint k = 2; k < tested; ++k) {
if (tested % k == 0) {
res = false;
break;
}
}
isPrime[tested] = res ? 1 : 0;
done[tested] = 1;
}
}
);
}

for (auto & t : threads) {
t.join();
}

auto end = std::chrono::system_clock::now();
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

uint num = std::count_if(isPrime.begin(), isPrime.end(), [](char b){return b != 0;});
uint nDone = std::count_if(done.begin(), done.end(), [](char b){return b == 0;});

std::cout << "number: " << num << " duration: " << milliseconds.count() << '\n';
std::cout << "not done: " << nDone << '\n';
for (uint i = 0; i < MAX; ++i) {
if (done[i] == 0) {
std::cout << i << ", ";
}
}
std::cout << '\n';
return 0;
}

补充说明

  • 你的taken.fetch_add(1)完全没问题:原子操作保证了每个线程都会拿到唯一的tested值,不会跳过任何数字。
  • std::thread::join()会自动完成内存同步,主线程在join之后能看到所有线程的写入,不需要额外加内存屏障。

内容的提问来源于stack exchange,提问作者Petr Král

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:26:42