-
RWM操作中含有两种操作,一个load操作,另一个store操作。然而,RWM操作的函数只支持传入一个内存序形参来同时表示store操作和load操作(那些CAS操作能够传入两个内存序形参并不是分别给load操作和store操作用的,是分别对应操作失败和操作成功的),因此,如果RWM函数中内存序传入的是
acquire
,consume
内存序,则此内存序只能对RWM中的load部分有效,如果传入的是release
,则只对RWM中的store有效,如果传入的是acq_rel
或者seq_cst
,则能同时对RWM中的load和store生效,并且其中的load对应的就是acquire
,store对应的就是release
; -
如果
seq_cst
与acquire
或者release
配对使用(即seq_cst
修饰同一对同步点中的一个,acquire
或者release
修饰这一对同步点中的另一个),则如果seq_cst
修饰的是store操作,则此store操作相当于用的是release
语义,如果seq_cst
修饰的是load操作,则此load操作相当于用的是acquire
语义; -
对于函数
std::atomic_thread_fence()
,如果传入release标记的话,此fence函数应该放在store函数的前面,如果传入的是acquire标记的话,此fence应该放在load函数的后面。此外,两个fence同步的话,同步点在fence处,因此fence函数只能对处于fence两边的内存操作的顺序进行约束,当内存操作都处于fence函数的同一边时,是起不到约束作用的; -
如果原子操作已经发生了
happences-before
关系,则fence函数并不影响其已经产生的关系; -
原子操作能阻止数据竞争,但是不能阻止条件竞争,数据竞争可能会产生未定义行为。关于数据竞争和条件竞争的区别,请看博文;
-
注意,在使用原子变量之前,最好先使用is_lock_free()判断一下其内部是否是用锁实现的,如果是的,那么最好不要使用原子变量,而是直接就使用锁,因为锁更不容易出错,且容易维护。这种情况下原子操作的效率并不比锁的效率高。当然,最好是使用相关的宏,直接在编译期就能确定是不是,这样能写出效率更好的代码。此外,C++17的is_always_lock_free()函数也是编译期执行的,也可优先考虑使用;
-
不要把
std::atomic<>
与那些可选名称如std::atomic_bool
等混用,因为可能导致代码不可移植。尽量使用std::atomic<>
而不是那些可选的替代类型(除非是在C接口中使用); -
原子类型的那些复合赋值操作符(如
+=
)返回的是当前原子变量存储的新值,而这些操作符对应的成员函数版本(如fetch_add()
)返回的是原子变量的旧值。例外情况是,前置递增(或递减)运算符返回的是新值,后置递增(或递减)运算符返回的是旧值。注意,它们返回的全都不是引用,而是右值; -
std::atomic_flag
必须用ATOMIC_FLAG_INIT
初始化,它将std::atomic_flag
初始化为clear状态,test_and_set()
能将其转成set状态,而clear()
能将其恢复成clear状态; -
注意,在弱CAS的平台上(即平台不支持直接使用CPU指令来实现原子类型的比较并交换(compare-and-swap)操作),
compare_exchange_strong()
函数可能内部是使用compare_exchange_weak()
函数并加上一个循环来实现的。因此,如果用compare_exchange_strong()
函数时要主动加一个循环,可以考虑看看能不能直接使用compare_exchange_weak()
函数,这样或许能少加一层循环; -
compare_exchange_xxx()
函数可以传入两个内存序标记,一个是用于指定函数返回true时(表示进行了store动作)的内存序操作,一个是用于指定返回false时(表示未进行store动作)的内存序操作。第一个内存序形参是用于指定返回true时(函数执行成功)的内存序操作,第二个是用于返回false(函数执行失败)的。当传入两个内存序标记时,成功的内存序的限制级别必须大于等于失败的内存序限制级别,不能小于。由于失败时没有进行store操作,因此失败的内存序标记不能是release
或者acq_rel
,可以指定为其他任何类型,包括relaxed。此外,当然也可以只传入一个内存序形参,只传入一个的话,它是用于指定成功的内存序操作的。此时,失败的内存序会自动使用与成功相同的内存序标记,不过会把其上的release
部分的语义给排除掉(也就是语义退化),比如如果只传入一个release内存序用于成功,则失败的内存序(第二个内存序形参)会自动变成relaxed
(将release
退化成relaxed
),如果是acq_rel
用于成功,则失败的自动变成acquire
(将acq_rel
退化成acquire
),如果两个都不指定,则成功和失败都统一使用seq_cst
; -
整形原子变量没有乘法、除法和位移动操作符;
-
如果想把
std::atomic<>
模板用于自定义类型(UDT),则UDT不能含有虚函数,不能有虚基类,且赋值运算符必须是编译器合成的。此外,UDT的基类以及它的非静态数据成员也必须符合这些条件。这些条件会允许编译器使用memcpy
或者等价的操作(也就是连续内存拷贝)来执行赋值操作。最后,UDT原子类型的compare_exchange操作必须可以用按位比较(就像使用memcmp()
一样),而不能有任何自定义的比较操作。如果UDT类型提供的比较操作有不同的语义,或者其中含有一些填充bit位不参与比较,则即使数值比较上是相等的,compare_exchange操作也会失败。综合看起来,就UDT就像是C语言中的struct类似,就是存储一些数据的集合体,即使包含有内置类型数组都可以; -
注意,浮点原子类型在使用compare_exchange等函数时可能会出错,因为浮点数的表示形式可能不一样。此外,浮点原子类型没有任何算数运算操作(比如+=,-=等);
-
C++标准特地为
std::shared_ptr<>
准备了全局原子操作函数的重载版本,所以在多线程之间使用std::shared_ptr<>
时应该使用原子操作函数来操作。不过,没有std::shared_ptr<>
的原子类型,其存储的类型仍然是std::shared_ptr<>
(至少C++17标准中还没有),但是在存储的时候或者获取的时候要调用相应的全局原子操作函数,例如:// 智能指针,用于在线程之间传递数据 std::shared_ptr<my_data> p; void update_global_data() { std::shared_ptr<my_data> local(new my_data); // 1 必须使用全局原子写函数来存储 std::atomic_store(&p, local); } void process_global_data() { // 2 在使用的时候,也要先通过全局原子读函数将其转成普通智能指针,再使用 std::shared_ptr<my_data> local = std::atomic_load(&p); process_data(local); }
-
一般来说,在同一个表达式中的多个操作之间的顺序是不确定的,但有些是确定的,比如逗号表达式,或者一个表达式的结果作为另一个表达式的参数时。