无锁数据结构二-乱序控制(栅栏)
内存栅栏
由于优化会导致对代码的乱序执行,在并发执行时可能带来问题。因此为了并行代码的正确执行,我们需提示处理器对代码优化做一些限制。而这些提示就是内存栅栏(memory barriers),用来对内存访问进行管理。要详细了解内存栅栏原理及产生原因,可参考无锁数据结构(基础篇):内存栅障。每种处理器架构都能提供一组完整的内存栅栏供开发使用,使用这些,我们能建立不同的内存模型。通过内存模型,我们能控制并发的执行顺序,也即同步。
下面先对乱序进行一些介绍,可参考3,4
乱序
乱序分为编译器乱序和处理器(cpu)乱序,下面是我对它们的一些了解。
编译器乱序
- 静态分析乱序;
- 可从全局视图进行乱序(跨过程);
处理器乱序
- 处理器可以获知程序的运行时动态行为,可以动态对指令进行乱序执行。
- 乱序范围小,只有有限分析范围。
由于乱序可在不同层进行,栅栏也有多种,可分为编译器内存栅栏(编译器),内存栅栏(cpu)。对于c++11 来说其定义内存模型,std::memory_order对编译器和cpu都会施加影响。下面主要介绍c++11的内存模型。
内存模型与执行顺序
有如下三种内存模型:
- 序列一致性模型
- 释放/获取语义模型
- 宽松的内存序列化模型
可分成四种执行顺序
- 序列一致顺序(Sequentially-consistent ordering)
- 释放获取顺序(Release-Acquire ordering)
- 释放消费顺序(Release-Consume ordering)
- 宽松顺序(Relaxed ordering)
所有这些内存模型定义在一个C++列表中– std::memory_order,包含以下六个常量:
- memory_order_seq_cst 指向序列一致性模型
- memory_order_acquire, memory_order_release,memory_order_acq_rel, memory_order_consume 指向基于获取/释放语义的模型
- memory_order_relaxed 指向宽松的内存序列化模型
其中:
- memory_order_relaxed 只保证此操作是原子的,不保证任何读写内存顺序。
- memory_order_consume 对于当前线程依赖于当前原子变量A的变量的读或写不能重排到此位置前,其他线程对依赖于A的变量的在释放A前(如store(memory_order_release))的读写,对于当前线程都是可见的(释放/消费语义)。在大多数平台上,这只影响到编译器优化> 此语义作为一个“内存的礼物”被引入DECAlpha处理器中。
- memory_order_acquire 带此内存顺序的加载操作,在其影响的内存位置进行获得操作:当前线程中读或写不能能被重排到此加载前。其他释放同一原子变量的线程的所有写入,为当前线程所可见(释放/获取语义)。与memory_order_consume区别:此标志影响所有变量读写,而consume影响依赖原子变量的读写。
- memory_order_release 带此内存顺序的存储操作进行释放操作:当前进程中的读或写不能被重排到此存储后。当前线程的所有写入,可见于获得(acquire)该同一原子变量的其他线程(释放/获取语义),并且对该原子变量的带依赖写入变得对于其他消费同一原子对象的线程可见(释放/消费语义)。
- memory_order_acq_rel 带此内存顺序的读-修改-写操作既是获得操作又是释放操作。当前线程的读或写内存不能被重排到此存储前或后。所有释放同一原子变量的线程的写入可见于修改之前,而且修改可见于其他获得同一原子变量的线程。
- memory_order_seq_cst 任何带此内存顺序的操作既是获得操作又是释放操作,加上存在一个单独全序,其中所有线程以同一顺序观测到所有修改(序列一致)。
针对读(加载),可选memory_order_acquire和 memory_order_consume。针对写(存储),仅能选memory_order_release。Memory_order_acq_rel是唯一可以用来做RMW运算,比如compare_exchange, exchange, fetch_xxx。事实上,因为RMW可以并发执行原子读\写,原子性RMW原语拥有获取语义memory_order_acquire, 释放语义memory_order_release 或者 memory_order_acq_rel.
-memory_order_acq_rel – is somehow similar to memory_order_seq_cst, but RMW-operation is located inside the acquire/release-section -memory_order_relaxed – RMW-operation shifting (its load and store parts) upwards/downwards the code (for example, within the acquire/release section, if the operation is located inside such section) doesn’t lead to errors
c++11可通过下列语句组合实现内存模型
std::atomic::load
syd::atomic::store
...
...
std::atomic_thread_fence
- 1
- 2
- 3
- 4
- 5
一般情况下上图左右(store(memory_order_release)
与atomic_thread_fence(memory_order_release)
)可以互相替换,但其仍有区别,Release Fence更加严格,其阻止下方的store穿过它,而Release Operation不行,如下,m_instance的store可在g_dummy.store上:
Singleton* tmp = new Singleton;
g_dummy.store(0, std::memory_order_release);
m_instance.store(tmp, std::memory_order_relaxed);
- 1
- 2
- 3
为简单,在介绍模型时只介绍load/store
下面是我对各模型的理解
序列一致顺序
这是一种严格的内存模型,它确保处理器按程序本身既定顺序执行。
带标签 memory_order_seq_cst 的原子操作不仅以与释放/获得顺序相同的方式排序内存(在一个线程中发生先于存储的任何结果都变成做加载的线程中的可见副效应),还对所有拥有此标签的内存操作建立一个单独全序。
释放获取顺序
同步仅建立在释放和获得同一原子对象的线程之间。其他线程可能看到与被同步线程的一者或两者相异的内存访问顺序。
在强顺序系统( x86 、 SPARC TSO 、 IBM 主框架)上,释放获得顺序对于多数操作是自动进行的。无需为此同步模式添加额外的 CPU 指令,只有某些编译器优化受影响(例如,编译器被禁止将非原子存储移到原子存储-释放后,或将非原子加载移到原子加载-获得前)。在弱顺序系统( ARM 、 Itanium 、 Power PC )上,必须使用特别的 CPU 加载或内存栅栏指令。
通过下面代码来说明此模型:
#include <thread>
#include <atomic>
#include <cassert>
#include <vector>std::vector<int> data;
std::atomic<int> flag = {0};void thread_1()
{data.push_back(42);flag.store(1, std::memory_order_release);//memory_order_release 保证data.push_back(42)执行顺序一定在store前
}void thread_2()
{int expected=1;while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) { //memory_order_acq_rel具有release和acquire,int expected=1;在前,expected = 1;在后//只有当thread_1的store执行后才会进入循环,此时能保证data==42,同时memory_order_acq_relexpected = 1; }
}void thread_3()
{while (flag.load(std::memory_order_acquire) < 2);//只有在执行了flag.compare_exchange_strong后,才跳出循环,此时已保证data==42assert(data.at(0) == 42); // 决不出错
}int main()
{std::thread a(thread_1);std::thread b(thread_2);std::thread c(thread_3);a.join(); b.join(); c.join();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
释放消费顺序
使用memory_order_consume
替换memory_order_acquire
来提供比memory_order_acquire
更弱的控制。
同步仅在释放和消费同一原子对象的线程间建立。其他线程能见到与被同步线程的一者或两者相异的内存访问顺序。
所有异于 DEC Alphi 的主流 CPU 上,依赖顺序是自动的,无需为此同步模式产生附加的 CPU 指令,只有某些编译器优化收益受影响(例如,编译器被禁止牵涉到依赖链的对象上的推测性加载)。
注意当前(2015年2月)没有产品编译器跟踪依赖链:消费操作被提升成获得操作。
释放消费顺序的规范正在修订中,而且暂时不鼓励使用 memory_order_consume (C++17 起)
例子如下:
#include <thread>
#include <atomic>
#include <cassert>
#include <string>std::atomic<std::string*> ptr;
int data;void producer()
{std::string* p = new std::string("Hello");data = 42;ptr.store(p, std::memory_order_release);
}void consumer()
{std::string* p2;while (!(p2 = ptr.load(std::memory_order_consume)));assert(*p2 == "Hello"); // 绝无出错: *p2 从 ptr 携带依赖assert(data == 42); // 可能也可能不会出错: data 不从 ptr 携带依赖,可能在ptr.load前执行。
}int main()
{std::thread t1(producer);std::thread t2(consumer);t1.join(); t2.join();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
此顺序的典型使用情况,涉及对很少被写入的数据结构(安排表、配置、安全策略、防火墙规则等)的共时读取,和有指针中介发布的发布者-订阅者情形,即当生产者发布消费者能通过其访问信息的指针之时:无需令生产者写入内存的所有其他内容对消费者可见(这在弱顺序架构上可能是昂贵的操作)。这种场景的一个例子是 rcu 解引用.
宽松顺序
提供最弱控制,只保证原子性
典型使用是计数器自增,例如std::shared_ptr 的引用计数器,因为这只要求原子性,但不要求顺序或同步
参考文档
- std::memory_order
- 无锁数据结构(基础篇):内存模型[en]
- 当目标CPU具有乱序执行的能力时,编译器做指令重排序优化的意义有多大?
- 内存栅障
- Acquire and Release Fences Don’t Work the Way You’d Expect
- 关于Singleton中使用DCLP和Memory barrier的一点疑问?
- Memory Barriers Are Like Source Control Operations
无锁数据结构二-乱序控制(栅栏)相关推荐
- c/c++多线程编程与无锁数据结构漫谈
本文主要针对c/c++,系统主要针对linux.本文引述别人的资料均在引述段落加以声明. 场景: thread...1...2...3...:多线程遍历 thread...a...b...c...:多 ...
- 无锁数据结构三:无锁数据结构的两大问题
实现无锁数据结构最困难的两个问题是ABA问题和内存回收问题.它们之间存在着一定的关联:一般内存回收问题的解决方案,可以作为解决ABA问题的一种只需很少开销或者根本不需额外开销的方法,但也存在一些情况并 ...
- 无锁数据结构--理解CAS、ABA、环形数组
在分布式系统中经常会使用到共享内存,然后多个进程并行读写同一块共享内存,这样就会造成并发冲突的问题, 一般的常规做法是加锁,但是锁对性能的影响非常大. 无锁队列是一个非常经典的并行计算数据结构,它极大 ...
- java无锁数据结构,无锁有序链表的实现
感谢同事[kevinlynx]在本站发表此文 无锁有序链表可以保证元素的唯一性,使其可用于哈希表的桶,甚至直接作为一个效率不那么高的map.普通链表的无锁实现相对简单点,因为插入元素可以在表头插,而有 ...
- 实用的无锁队列(二)
上次的修改一下 链接在此:[无锁队列一] //c_buffer.h class c_data {private:s_memory v_root = NULL;s_memory *v_r = NULL; ...
- 《C++ Concurrency in Action》笔记28 无锁并行数据结构
7 设计无锁并行数据结构 mutex是一种强大的工具,可以保证多个线程安全访问数据结构.使用mutex的目的很直接:访问被保护数据的代码要么锁定了mutex,要么没有.然而,它也有不好的一面,错误的使 ...
- C++线程编程-设计无锁的并发数据结构
定义和结果 使用互斥元.条件变量以及future 来同步数据的算法和数据结构被称为阻塞的算法和数据结构.调用库函数的应用会中断一个线程的执行,直到另一个线程执行一个动作.这种库函数调用被称为阻塞调用, ...
- 上篇 | 说说无锁(Lock-Free)编程那些事
1. 引言 现代计算机,即使很小的智能机亦或者平板电脑,都是一个多核(多CPU)处理设备,如何充分利用多核CPU资源,以达到单机性能的极大化成为我们码农进行软件开发的痛点和难点.在多核服务器中,采用多 ...
- 理解 Memory barrier(内存屏障)无锁环形队列
Memory barrier 简介 程序在运行时内存实际的访问顺序和程序代码编写的访问顺序不一定一致,这就是内存乱序访问.内存乱序访问行为出现的理由是为了提升程序运行时的性能.内存乱序访问主要发生在两 ...
最新文章
- 白噪音和粉红噪音煲机_白噪音真的有助于睡眠?这款可以自定义的应用给你答案...
- 倒序查询_mysql大表分页查询翻页优化方案
- zabbix开启报警声音 网页也可以有声音
- python3怎么读取excel_python3 读取excel
- Bootstrap3 输入提示插件typeahead
- 初学者python笔记(类的内置属性)
- Hadoop之自定义数据类型
- 微PE系统安装包下载及安装教程,纯净微pe系统安装
- 词法分析——词法分析器的作用
- MTK 6589暗码切换开机LOGO(不适应NAND 的FLASH)
- PS CS6教程(photoshop视频教程) 免费下载
- 微信公众平台 登陆php,javascript - 微信公众号开发,如何使用户保持登录状态
- [英语阅读]希腊古剧场对高跟鞋说“不”
- 物理PC机ping不通虚拟机解决方法(亲测可用)
- 方差、协方差、协方差矩阵以及互相关矩阵
- [附源码]Node.js计算机毕业设计高校心理咨询管理系统Express
- 我,程序员,马上35岁...
- 从开发转到安全渗透工程师,是我做的最对的决定
- BT5R3下安装metasploit
- react中将json对象转换为数组
热门文章
- php识别名片,用户信息名片怎么利用PHP实现自动生成
- 现实生活中常用的动态路由—OSPF路由重分发
- 内存不能为读写的解决方法
- 单片机产生可调方波(c语言),单片机产生占空比可调方波(PWM)
- matlab引擎函数,Matlab引擎库函数
- mysql putty 备份_Linux下mysql数据库的备份-putty
- 最小二乘多项式拟合程序matlab,最小二乘法的多项式拟合(matlab实现)
- qsettings mysql_qt连接mysql
- cdh用户权限_cdh设置hdfs权限
- html怎么添加图片幻灯,使用CSS3实现的超酷幻灯图片效果