见下面的代码,
AsyncTask
创建一个对等线程(定时器)来增加一个原子变量并休眠一段时间。预期的输出是打印10次counter_
,取值范围为1到10,但实际结果很奇怪:
#include <atomic>
#include <thread>
#include <iostream>
class AtomicTest {
public:
int AsyncTask() {
std::thread timer([this](){
while (not stop_.load(std::memory_order_acquire)) {
counter_.fetch_add(1, std::memory_order_relaxed);
std::cout << "counter = " << counter_ << std::endl;
std::this_thread::sleep_for(std::chrono::microseconds(1)); // both milliseconds and seconds work well
}
});
timer.detach();
std::this_thread::sleep_for(std::chrono::microseconds(10));
stop_.store(true, std::memory_order_release);
return 0;
}
private:
std::atomic<int> counter_{0};
std::atomic<bool> stop_{false};
};
int main(void) {
AtomicTest test;
test.AsyncTask();
return 0;
}
我知道线程切换也是需要时间的,难道是线程休眠时间太短了?
我的程序运行环境:
是的,
stop_.store
可以在新线程被调度到 CPU 内核之前或之后运行,这是很容易理解的。所以它的第一个测试将停止标志读取为true
.
10 us 比典型的操作系统进程调度时间片(通常为 1 或 10 毫秒)短,以防相关。对于原子存储变得可见,只比内核间延迟高几个数量级。
你描述的结果正是我对像这样的依赖于时间的程序的期望,编写它是为了检测哪个线程赢得了比赛以及赢得了多少(它的速度慢
<< endl
并在写入线程内休眠。)
我绝对不希望它总是打印 10 次,而且由于线程启动开销占打印线程内 1 us 睡眠间隔的很大一部分,这种情况很少发生。
顺便说一句,您的问题最初的标题是“关于递增原子变量的问题?”。但是
counter
只能从一个线程访问。它可能与停止标志在同一个缓存行中,但如果没有来自主线程的争用,它基本上是微不足道的,一个非常快速的操作。
与你在做什么无关;它可能是线程 lambda 内部的局部非原子
int
,您会看到相同的计时效果。这里重要的事情是 cout << endl
强制刷新流(因此系统调用),即使您重定向到文件,以及 this_thread::sleep_for()
.
如果 write 系统调用是针对终端(而不是重定向到文件),它甚至可能会在终端仿真器在屏幕上绘制时阻塞,尽管只有几个小的写入可能在某处(可能在内核内部)有足够大的缓冲区) 吸收它。
一个原子增量可能需要几纳秒,而
relaxed
AArch64 可以非常有效地处理它,大部分时间与周围的代码重叠。 (现代 x86 最多可以每 20 个时钟周期执行一次原子增量,这包括一个完整的内存屏障。我希望 Apple M1 在不需要成为屏障时能够更便宜地处理它。)