浅谈关于C++memory_order的理解

看了c++并发编程实战的内存模型部分后,一直对memory_order不太懂,今天在知乎发现了百度的brpc,恰好有关于原子操作的文档,感觉解释的很好。为了加深理解,再次总结一遍。

在多核编程中,我们使用锁来避免多个线程修改同一个数据时产生的竞争条件。但是,锁会消耗系统资源,当锁成为性能瓶颈的时候,就需要使用另一种方法——原子指令。c++11中引入了原子类型atomic

原子指令 (x均为std::atomic)

作用x.load()返回x的值。x.store(n)把x设为n,什么都不返回。x.exchange(n)把x设为n,返回设定之前的值。x.compare_exchange_strong(expected_ref, desired)若x等于expected_ref,则设为desired,返回成功;否则把最新值写入expected_ref,返回失败。x.compare_exchange_weak(expected_ref, desired)相比compare_exchange_strong可能有spurious wakeup。x.fetch_add(n), x.fetch_sub(n)原子地做x += n, x-= n,返回修改之前的值。

但仅靠原子指令实现不了对资源的访问控制。这造成的原因是编译器和cpu实施了重排指令,导致读写顺序会发生变化,只要不存在依赖,代码中后面的指令可能会被放在前面,从而先执行它。cpu这么做是为了尽量塞满每个时钟周期,在单位时间内尽量执行更多的指令,从而提高吞吐率。

下面看个例子:

// thread 1
// ready was initialized to false
p.init();
ready = true;
// thread 2
if(ready) {
 p.bar();
}

线程2在ready为true的时候会访问p,对线程1来说,如果按照正常的执行顺序,那么p先被初始化,然后在将ready赋为true。但对多核的机器而言,情况可能有所变化:

  • 线程1中的ready = true可能会被cpu或编译器重排到p.init()的前面,从而优先执行ready = true这条指令。在线程2中,p.bar()中的一些代码可能被重排到if(ready)之前。
  • 即使没有重排,ready和p的值也会独立地同步到线程2所在核心的cache,线程2仍然可能在看到ready为true时看到未初始化的p。

为了解决这个问题,cpu和编译器提供了memory fence,让用户可以声明访存指令的可见性关系,c++11总结为以下memory order:

memory order 作用
memory_order_relaxed 无fencing作用,cpu和编译器可以重排指令
memory_order_consume 后面依赖此原子变量的访存指令勿重排至此条指令之前
memory_order_acquire 后面访存指令勿重排至此条指令之前
memory_order_release 前面的访存指令勿排到此条指令之后。当此条指令的结果被同步到其他核的cache中时,保证前面的指令也已经被同步。
memory_order_acq_rel acquare + release
memory_order_seq_cst acq_rel + 所有使用seq_cst的指令有严格的全序关系

有了memoryorder,我们可以这么改上面的例子:

// Thread1
// ready was initialized to false
p.init();
ready.store(true, std::memory_order_release);
// Thread2
if (ready.load(std::memory_order_acquire)) {
  p.bar();
}

线程2中的acquire和线程1的release配对,确保线程2在看到ready==true时能看到线程1 release之前所有的访存操作。

注意,memory fence不等于可见性,即使线程2恰好在线程1在把ready设置为true后读取了ready也不意味着它能看到true,因为同步cache是有延时的。memory fence保证的是可见性的顺序:“假如我看到了a的最新值,那么我一定也得看到b的最新值”。

到此这篇关于浅谈关于C++memory_order的理解的文章就介绍到这了,更多相关C++ memory order内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解C++中的内存同步模式(memory order)

    内存模型中的同步模式(memory model synchronization modes) 原子变量同步是内存模型中最让人感到困惑的地方.原子(atomic)变量的主要作用就是同步多线程间的共享内存访问,一般来讲,某个线程会创建一些数据,然后给原子变量设置标志数值(译注:此处的原子变量类似于一个flag);其他线程则读取这个原子变量,当发现其数值变为了标志数值之后,之前线程中的共享数据就应该已经创建完成并且可以在当前线程中进行读取了.不同的内存同步模式标识了线程间数据共享机制的"强弱"

  • 浅谈关于C++memory_order的理解

    看了c++并发编程实战的内存模型部分后,一直对memory_order不太懂,今天在知乎发现了百度的brpc,恰好有关于原子操作的文档,感觉解释的很好.为了加深理解,再次总结一遍. 在多核编程中,我们使用锁来避免多个线程修改同一个数据时产生的竞争条件.但是,锁会消耗系统资源,当锁成为性能瓶颈的时候,就需要使用另一种方法--原子指令.c++11中引入了原子类型atomic. 原子指令 (x均为std::atomic) 作用x.load()返回x的值.x.store(n)把x设为n,什么都不返回.x

  • 浅谈对yield的初步理解

    如下所示: def go(): while True: data = 1 r = yield data # data是返回值,r是接收值 print("data", data) print("A1", r) data += 1 r = yield data print("data",data) r += r print("A2", r) data += 1 r = yield data print("data&quo

  • 浅谈对于DAO设计模式的理解

    为了降低耦合性,提出了DAO封装数据库操作的设计模式. 它可以实现业务逻辑与数据库访问相分离.相对来说,数据库是比较稳定的,其中DAO组件依赖于数据库系统,提供数据库访问的接口. 一般的DAO的封装由以下另个原则:  · 一个表对应一个表,相应地封装一个DAO类.  · 对于DAO接口,必须由具体的类型定义.这样可以避免被错误地调用. 在DAO模式中,将对数据的持久化抽取到DAO层,暴露出Service层让程序员使用,这样,一方面避免了业务代码中混杂JDBC调用语句,使得业务落实实现更加清晰.

  • 浅谈TypeScript 索引签名的理解

    目录 1.什么是索引签名 2. 索引签名语法 3. 索引签名的注意事项 3.1不存在的属性 3.2 string 和 number 键 4.索引签名与 Record<Keys, Type>对比 我们用两个对象来描述两个码农的工资: const salary1 = { baseSalary: 100_000, yearlyBonus: 20_000 }; const salary2 = { contractSalary: 110_000 }; 然后写一个获取总工资的函数 function tot

  • 浅谈对c# 面向对象的理解

    一.了解面向对象 1.概念基本理解:1).一个个体可以看做是一个对象,例如:人这个个体: 2).有共同属性的一类作为一个个体,例如:学生.白领.农民工: 3).结构体是用户自定义的数据类型,可以定义不同数据类型的变量,结构体也是面向对象的核心: 2.基本特性: 1)封装:是隐藏信息的特性,具有"封装"意识,是掌握面向对象分析与设计技巧的关键. 最简单的理解:创建一个对象的整体,使对象的属性可以具有赋值.取值的功能,也就是对象中的变量可以赋值.取值.,是一种认为的抽象出来的对象的概念.

  • 浅谈Java slf4j日志简单理解

    一.理解 slf4j(Simple Logging Facade for Java),表示为java提供的简单日志门面,更底层一点说就是接口.通过将程序中的信息导入到日志系统并记录,实现程序和日志系统的解耦 日志门面接口本身通常并没有实际的日志输出能力,它底层还是需要去调用具体的日志框架API的,也就是实际上它需要跟具体的日志框架结合使用.由于具体日志框架比较多,而且互相也大都不兼容,日志门面接口要想实现与任意日志框架结合可能需要对应的桥接器,就好像JDBC与各种不同的数据库之间的结合需要对应的

  • 浅谈Spring中IOC的理解和认知

    IOC的推导 1.1.模拟一个正常查询信息的业务流程: ①mapper层:因为没有连接数据库,这里我们写一个mapper的实现类来模拟数据的查询 public interface PerMapper { void getPerInfo(); } public class StudentMapperImpl implements PerMapper { @Override public void getPerInfo() { System.out.println("我是一个学生"); }

  • 浅谈对Lambda表达式的理解

    在.NET 1.0的时候,大家都知道我们经常用到的是委托.有了委托呢,我们就可以像传递变量一样的传递方法.在一定程序上来讲,委托是一种强类型的托管的方法指 针,曾经也一时被我们用的那叫一个广泛呀,但是总的来说委托使用起来还是有一些繁琐.来看看使用一个委托一共要以下几个步骤: 用delegate关键字创建一个委托,包括声明返回值和参数类型 使用的地方接收这个委托 创建这个委托的实例并指定一个返回值和参数类型匹配的方法传递过去 好啦,我承认啦上面是自己在网上看到的,但是它很好的介绍了委托,在以前要使

  • 浅谈virtual、abstract方法和静态方法、静态变量理解

    说点对这几个容易混淆的词的理解: 1.c++中的virtual方法的 virtual关键字主要是防止继承中重复继承父类的同一个方法而设置的标识. 2.virtual与abstract关键字的不同之处在于 virtual方法可以有具体的实现,当子类继承父类的时候若没有覆写该方法,也可以使用父类中的此方法. 但是abstract方法即抽象方法是没有具体实现的,子类需要自己实现.打个比方就是virtual 虚方法 这个 父亲虽然"虚"了点但'家产'还是有一点的,但老爸是抽象方法这个儿子就悲剧

  • 浅谈Java 对于继承的初级理解

    概念:继承,是指一个类的定义可以基于另外一个已存在的类,即子类继承父类,从而实现父类的代码的重用.两个类的关系:父类一般具有各个子类共性的特征,而子类可以增加一些更具个性的方法.类的继承具有传递性,即子类还可以继续派生子类,位于上层的类概念更加抽象,位于下层的类的概念更加具体. 1.定义子类: 语法格式 [修饰符] class 子类名 extends 父类名{ 子类体 } 修饰符:public private protected default 子类体是子类在继承父类的内容基础上添加的新的特有内

随机推荐