C++(标准库):51---并发之(原子操作:atomic)
发布日期:2021-06-29 22:36:24 浏览次数:3 分类:技术文章

本文共 5027 字,大约阅读时间需要 16 分钟。

  • 关于原子操作的实现原理在另一篇博客中有详细的介绍,可以参阅:
  • 本文先介绍atomic的高层接口:它所提供的操作将使用默认保证,不论内存访问次序如何。这个默认保证提供了顺序一致性,意思是在线程之中atomic操作保证一定“像代码出现的次序”那样地发生
  • 然后再介绍atomic的底层接口:带有“放宽之次序保证”的操作
  • C++标准库并不区分atomic的高层或底层接口:
    • 底层是Hans Boehm说的,他是这个程序库的作者之一
    • 某些时候atomic底层接口也被称为weak或relaxed接口,而高层接口被称为normal或strong接口
  • gcc/g++编译器提供的原子操作可以参阅:
  • 本文只列出了C++部分的原子接口,更多详细内容可以参阅:

一、Atomic的使用案例

  • 我们先看看在前几篇文章中使用的案例:
    • thread1()和thread2()两个函数被不通过的线程调用执行
    • thread1()函数中对锁住muex,然后将readyFlag设置为true
    • thread2()函数会不断地对mutex进行加锁和解锁,等待readyFlag变为true
    • 因此,总体来说,就是thread2()不断地循环等待thread1()将某种条件设置为true
bool readyFlag;std::mutex readyFlagMutex; void thread1(){    //做一些thread2需要的准备工作    //...    std::lock_guard
lg(readyFlagMutex); readyFlag = true;} void thread2(){ //等待readyFlag变为true { std::unique_lock
ul(readyFlagMutex); //如果readyFlag仍未false,说明thread1还没有锁定,那么持续等待 while (!readyFlag) { ul.unlock(); std::this_thread::yield(); std:this_thread::sleep_for(std::chrono::milliseconds(100)); ul.lock(); } }//释放lock //在thread1锁定之后,做相应的事情}
  • 上面的演示案例我们为了保证程序的并发性,使用了mutex对共享数据进行保护访问。但是我们也可以使用原子操作来对共享数据进行操作。代码修改如下:
/原子变量,其类型为bool类型std::atomic
readyFlag(false);void thread1(){ //原子地将readyFlag设置为true readyFlag.store(true);}void thread2(){ //每次原子地判断readyFlag为true还是false //load()返回readyFlag中的值 while (!readyFlag.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); }}

二、生产者消费者演示案例

#include 
#include
#include
#include
#include
using namespace std;long data;std::atomic
readyFlag(false);void provider(){ std::cout << "
" << std::endl; std::cin.get(); ::data = 42; //原子地设置readyFlag readyFlag.store(true);}void consumber(){ //原子的检查readyFlag为true还是false while (!readyFlag.load()) { std::cout.put('.').flush(); this_thread::sleep_for(std::chrono::seconds(1)); } std::cout << "\nvalue:" << ::data << std::endl;}int main(){ auto p = std::async(std::launch::async, provider); auto c = std::async(std::launch::async, consumber); this_thread::sleep_for(std::chrono::minutes(1));}

三、atomic的高层接口

  • atomic<>是一个模板,可以适用于任何一般类型上。另外特化版本针对于bool、所有整数类型以及pointer:

atomic的高层操作

  • 下图列出了atomic支持的高层操作。如果可能它们将直接映射至相关的CPU命令
  • 相关说明:
    • triv列表示:针对std::atomic<bool>及“其他普通类型之atomic”提供的操作
    • int type列表示:针对std::atomic<>且使用整数类型而提供的操作
    • ptr type列表示:针对std::atomic<>且使用pointer类型而提供的操作

  • 一些知识点的补充:
    • 一般而言,这些操作获得的是copy而不是reference
    • Default构造函数并未能够完全将object初始化。Default构造函数之后唯一合法的操作就是调用atomic_init()完成初始化
    • 接受相关类型值的那个构造函数并不是atomic
    • 所有函数,除了构造函数,都被重载为volatile和non-volatile两个版本。例如,atomic<int>之内声明了以下的赋值操作

is_lock_free()

  • 借助该函数,你可以检查atomic类型内部是否由于使用lock才成为atomic
  • 如果不是,你的硬件就是拥有对atomic操作的固有支持(那是“在signal handler内使用atomic”的一个必要条件)

conpare_exchange_strong()、conpare_exchange_weak()

  • 这两个函数都是所谓compare-and-swap(CAS)操作
  • CPU常常提供这个atomic操作用以比较“某内存区内容”和“某给定值”,并且唯有在它们相同时才将该内存区内容更新为另一给定的新值
  • 这可保证新值乃根据最新信息计算出来。这样的效果有点像以下的伪代码:

  • 因此,如果数值就在这一段时间里被另一线程更新,它会返回false并以expected承载新值
  • 上述两种形式中,weak形式有可能出现假失败,亦即期望值出现它仍然返回false。但是weak形式有时候比strong形式更高效

四、atomic的C-Style接口

  • 针对C++的atomic提案,C有一份对应提案,它应该提供相同语义但是(当然)不使用诸如template、reference和member function等C++特性。整个atomic接口有一个C-style对等品,称为C standard的一份扩充

例如

  • 你可以声明atomic_bool取代atomic<bool>,并替换store()和load,改用global函数,后者接受一个pointer指向对象

  • C另有一个接口,采用_Atomic和_Atomic(),因此C-style接口一般只用于“需要在C和C++之间保持兼容”的代码身上

C-style的atomic数据类型

  • 然而在C++中使用C-style atomic类型并不罕见
  • 下图列出了最重要的atomic类型名称,除此之外还有更多,适用于较不常见的类型,例如atomic_int_fast32_t乃是针对atomic<int_fast32_t>类型的

  • 针对shared_ptr还提供了特殊的atomic操作。原因是注入atomic<shared_ptr<T>>这样的声明不被允许,因为shared_ptr并非可被复制。Atomic操作遵循C-style接口的命名规范

五、atomic的底层接口

  • atomic底层接口意味着使用atomic操作时不保证顺序一致性。因此编译器和硬件有可能(局部)重排对atomic的处理次序
  • atomic底层操作如下图所示:
    • 如图所示,load、store、CAS等操作多提供了一个参数,允许你额外传递一个内存次序实参
    • 另外若干函数被额外提供出来,用以手动控制内存访问例如atomic_thread_fence()和atomic_signal_fence()被用来手动编写fence,那是“内存访问重安排”的界线

演示说明

  • 在文章上面生产者与消费者的演示案例如下:
long data;std::atomic
readyFlag(false);void provider(){ std::cout << "
" << std::endl; std::cin.get(); ::data = 42; readyFlag.store(true);}void consumber(){ while (!readyFlag.load()) { std::cout.put('.').flush(); this_thread::sleep_for(std::chrono::seconds(1)); } std::cout << "\nvalue:" << ::data << std::endl;}
  • 其中生产者rpovider()函数负责供应数据:

 

  • 消费者负责消费此数据

  • 我们使用默认的内存处理次序,于是保证顺序一致性。事实上,我们真正的调用如下图所示,其中调用函数都有一个默认实参std::memory_order_seq_cst(该参数用来执行内存次序,是成员函数的默认值)

  • 如果我们手动指定另一种内存处理次序,我们就可以削弱对次序的保证:
    • 在这个例子中我们可以要求provider不推迟atomic store之后的操作,而consumer不会在atomic之后带来向前操作
    • 代码如下:

  • 如果放宽(relaxing)atomic操作次序上的所有约束,会导致不明确的行为。代码如下,原因在于:
    • std::memoey_order_relaxed不保证此前所有内存操作在store发挥效用前都变得“可被其他线程看见”
    • 因此,provider线程有可能在设置ready flag之后才写data,于是consumer线程有可能在data正被写时读它,这就会造成data race

  • 你也修改一下代码,让data成为atomic并以std::memoey_order_relaxed作为内存次序。代码如下:
    • 严格来说,这并非不明确行为,因为我们并未遭遇data race。然而这却也难以预期般地运行,因为data的结果值有可能(尚未)不是42(memory order对此仍无保证)。其行为会导致data拥有一个无法具体说明的值

  • 只有当我们在atomic变量上读/写动作彼此独立,memory_order_relaxed才能显出用途。例如一个global计时器,不同的线程可能会对它累加或递减,而我们只需在所有线程终结之后获得该计数器的最终值即可
  • 本文没有对底层接口详细介绍,因为它们是为真正的并发专家或想成为专家人准备的
  • 如果想要仔细研究可以参与C++ Concurrency in Action的第5章和第7章。或其他资料

转载地址:https://dongshao.blog.csdn.net/article/details/105613517 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:C++(数据结构与算法):62---回溯法、回溯法应用(货箱装载、0/1背包问题、最大完备子图、旅行商问题、电路板排列)
下一篇:C++(标准库):50---并发之(条件变量:condition_variable、condition_variable_any)

发表评论

最新留言

网站不错 人气很旺了 加油
[***.192.178.218]2024年04月08日 13时04分55秒