由于 std::atomic 对象中包含的变量预计将从多个线程访问,因此应该期望它们至少表现得像使用 volatile 关键字声明的变量。
在 CPU 架构引入缓存行等功能之前,这是标准和推荐做法。
[EDIT2] 有人可能会认为 std::atomic<> 是多核时代的 volatile 变量。根据 C/C++ 的定义,volatile 只足以同步来自单个线程的原子读取,其中 ISR 修改变量(在这种情况下,从主线程看,实际上是原子写入)。
我个人很高兴没有编译器会优化掉对原子变量的写入。如果写入被优化掉了,如何保证其他线程中的读取者可以潜在地看到每个这样的写入呢?不要忘记这也是 std::atomic<> 合同的一部分。
考虑以下代码片段,其结果将受到编译器的疯狂优化的极大影响。
#include <atomic>
#include <thread>
static const int N{ 1000000 };
std::atomic<int> flag{1};
std::atomic<bool> do_run { true };
void write_1()
{
while (do_run.load())
{
flag = 1; flag = 1; flag = 1; flag = 1;
flag = 1; flag = 1; flag = 1; flag = 1;
flag = 1; flag = 1; flag = 1; flag = 1;
flag = 1; flag = 1; flag = 1; flag = 1;
}
}
void write_0()
{
while (do_run.load())
{
flag = -1; flag = -1; flag = -1; flag = -1;
}
}
int main(int argc, char** argv)
{
int counter{};
std::thread t0(&write_0);
std::thread t1(&write_1);
for (int i = 0; i < N; ++i)
{
counter += flag;
std::this_thread::yield();
}
do_run = false;
t0.join();
t1.join();
return counter;
}
[编辑] 起初,我并没有意识到volatile
对原子操作的实现是至关重要的,但是...
由于似乎有人怀疑volatile
与原子操作是否有关,所以我进行了调查。以下是来自VS2017 STL的原子实现。正如我所推测的那样,volatile
关键字被广泛使用。
template<unsigned _Bytes>
struct _Atomic_impl
{
typedef _Uint1_t _My_int;
constexpr _Atomic_impl() _NOEXCEPT
: _My_flag(0)
{
}
bool _Is_lock_free() const volatile
{
return (false);
}
void _Store(void *_Tgt, const void *_Src, memory_order _Order) volatile
{
_Atomic_copy(&_My_flag, _Bytes, _Tgt, _Src, _Order);
}
void _Load(void *_Tgt, const void *_Src,
memory_order _Order) const volatile
{
_Atomic_copy(&_My_flag, _Bytes, _Tgt, _Src, _Order);
}
void _Exchange(void *_Left, void *_Right, memory_order _Order) volatile
{
_Atomic_exchange(&_My_flag, _Bytes, _Left, _Right, _Order);
}
bool _Compare_exchange_weak(
void *_Tgt, void *_Exp, const void *_Value,
memory_order _Order1, memory_order _Order2) volatile
{
return (_Atomic_compare_exchange_weak(
&_My_flag, _Bytes, _Tgt, _Exp, _Value, _Order1, _Order2));
}
bool _Compare_exchange_strong(
void *_Tgt, void *_Exp, const void *_Value,
memory_order _Order1, memory_order _Order2) volatile
{
return (_Atomic_compare_exchange_strong(
&_My_flag, _Bytes, _Tgt, _Exp, _Value, _Order1, _Order2));
}
private:
mutable _Atomic_flag_t _My_flag;
};
MS stl 中的所有专业都在关键函数上使用 volatile。
以下是其中一个关键函数的声明:
inline int _Atomic_compare_exchange_strong_8(volatile _Uint8_t *_Tgt, _Uint8_t *_Exp, _Uint8_t _Value, memory_order _Order1, memory_order _Order2)
你会注意到需要的
volatile uint8_t*
将std::atomic中包含的值保存下来。这种模式可以在MS std::atomic<>实现中观察到,没有任何理由让gcc团队或其他STL提供者以不同的方式进行操作。
f
只是众多写入y
的线程中的一条,而还有其他线程在读取y
,那么如果编译器将写操作合并为单个写操作,则程序的行为可能会出现意外变化。 - Some programmer dudey
设置为42
的C++程序。你可以编写一个仅执行存储操作的程序,也许会有好运气,但无法保证它。不可能确定这种情况从未发生是因为多余的写入被删除了,还是只是由于时间不幸而导致的,因此优化是有效的。即使确实发生了这种情况,你也无法知道是在第一次、第二次还是第三次之前发生的。 - nwp