关于三种内存序的一些注意点



  1. RWM操作中含有两种操作,一个load操作,另一个store操作。然而,RWM操作的函数只支持传入一个内存序形参来同时表示store操作和load操作(那些CAS操作能够传入两个内存序形参并不是分别给load操作和store操作用的,是分别对应操作失败和操作成功的),因此,如果RWM函数中内存序传入的是acquireconsume内存序,则此内存序只能对RWM中的load部分有效,如果传入的是release,则只对RWM中的store有效,如果传入的是acq_rel或者seq_cst,则能同时对RWM中的load和store生效,并且其中的load对应的就是acquire,store对应的就是release

  2. 如果seq_cstacquire或者release配对使用(即seq_cst修饰同一对同步点中的一个,acquire或者release修饰这一对同步点中的另一个),则如果seq_cst修饰的是store操作,则此store操作相当于用的是release语义,如果seq_cst修饰的是load操作,则此load操作相当于用的是acquire语义;

  3. 对于函数std::atomic_thread_fence(),如果传入release标记的话,此fence函数应该放在store函数的前面,如果传入的是acquire标记的话,此fence应该放在load函数的后面。此外,两个fence同步的话,同步点在fence处,因此fence函数只能对处于fence两边的内存操作的顺序进行约束,当内存操作都处于fence函数的同一边时,是起不到约束作用的;

  4. 如果原子操作已经发生了happences-before关系,则fence函数并不影响其已经产生的关系;

  5. 原子操作能阻止数据竞争,但是不能阻止条件竞争,数据竞争可能会产生未定义行为。关于数据竞争和条件竞争的区别,请看博文

  6. 注意,在使用原子变量之前,最好先使用is_lock_free()判断一下其内部是否是用锁实现的,如果是的,那么最好不要使用原子变量,而是直接就使用锁,因为锁更不容易出错,且容易维护。这种情况下原子操作的效率并不比锁的效率高。当然,最好是使用相关的宏,直接在编译期就能确定是不是,这样能写出效率更好的代码。此外,C++17的is_always_lock_free()函数也是编译期执行的,也可优先考虑使用;

  7. 不要把std::atomic<>与那些可选名称如std::atomic_bool等混用,因为可能导致代码不可移植。尽量使用std::atomic<>而不是那些可选的替代类型(除非是在C接口中使用);

  8. 原子类型的那些复合赋值操作符(如+=)返回的是当前原子变量存储的新值,而这些操作符对应的成员函数版本(如fetch_add())返回的是原子变量的旧值。例外情况是,前置递增(或递减)运算符返回的是新值,后置递增(或递减)运算符返回的是旧值。注意,它们返回的全都不是引用,而是右值;

  9. std::atomic_flag必须用ATOMIC_FLAG_INIT初始化,它将std::atomic_flag初始化为clear状态,test_and_set()能将其转成set状态,而clear()能将其恢复成clear状态;

  10. 注意,在弱CAS的平台上(即平台不支持直接使用CPU指令来实现原子类型的比较并交换(compare-and-swap)操作),compare_exchange_strong()函数可能内部是使用 compare_exchange_weak()函数并加上一个循环来实现的。因此,如果用compare_exchange_strong()函数时要主动加一个循环,可以考虑看看能不能直接使用 compare_exchange_weak()函数,这样或许能少加一层循环;

  11. 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

  12. 整形原子变量没有乘法、除法和位移动操作符;

  13. 如果想把std::atomic<>模板用于自定义类型(UDT),则UDT不能含有虚函数,不能有虚基类,且赋值运算符必须是编译器合成的。此外,UDT的基类以及它的非静态数据成员也必须符合这些条件。这些条件会允许编译器使用memcpy或者等价的操作(也就是连续内存拷贝)来执行赋值操作。最后,UDT原子类型的compare_exchange操作必须可以用按位比较(就像使用memcmp()一样),而不能有任何自定义的比较操作。如果UDT类型提供的比较操作有不同的语义,或者其中含有一些填充bit位不参与比较,则即使数值比较上是相等的,compare_exchange操作也会失败。综合看起来,就UDT就像是C语言中的struct类似,就是存储一些数据的集合体,即使包含有内置类型数组都可以;

  14. 注意,浮点原子类型在使用compare_exchange等函数时可能会出错,因为浮点数的表示形式可能不一样。此外,浮点原子类型没有任何算数运算操作(比如+=,-=等);

  15. 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);
    }
    
    
  16. 一般来说,在同一个表达式中的多个操作之间的顺序是不确定的,但有些是确定的,比如逗号表达式,或者一个表达式的结果作为另一个表达式的参数时。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值