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
相关产品推荐
相关产品推荐

