从多核CPU Cache一致性的应用到分布式系统一致性的概念迁移
概述
现代多核CPU的cache模型基本都跟下图1所示一样,L1 L2 cache是每个核独占的,只有L3是共享的,当多个cpu读、写同一个变量时,就需要在多个cpu的cache之间同步数据,跟分布式系统一样,必然涉及到一致性的问题,只不过两者之间共享内容的方式不一样而已,一个通过共享内存来共享内容,另一个通过网络消息传递来共享内容。就像wiki所提及的:
Interestingly enough, a shared-memory multiprocessor system really is a message-passing computer under the covers. This means that clusters of SMP machines that use distributed shared memory are using message passing to implement shared memory at two different levels of the system architecture.
图1、现代cpu多级cache
多核一致性与原子操作
多核一致性最典型的应用场景是多线程的原子操作,其在多线程开发中经常用到,比如在计数器的生成,这类情况下数据有并发的危险,但是用锁去保护又显得有些浪费,所以原子类型操作十分的方便。
原子操作虽然用起来简单,但是其背景远比我们想象的要复杂。其主要在于现代计算系统过于的复杂:多处理器、多核处理器、处理器又有核心独有以及核心共享的多级缓存,在这种情况下,一个核心修改了某个变量,其他核心什么时候可见是一个十分严肃的问题。同时在极致最求性能的时代,处理器和编译器往往表现的很智能,进行极度的优化,比如什么乱序执行、指令重排等,虽然可以在当前上下文中做到很好的优化,但是放在多核环境下常常会引出新的问题来,这时候就必须提示编译器和处理器某种提示,告诉某些代码的执行顺序不能被优化。今天我们重点看一下处理器在多线程原子操作上的背景原理以及具体应用。
CPU Cache与内存屏障
考虑下面典型的代码:
-Thread -
void foo(void)
{
a = ;
b = ;
}
-Thread -
void bar(void)
{
while (b == ) continue;
assert(a == );
}
由于cpu cache的存在,thread 2在断言处可能会失败。具体的,由于各个CPU的cache是独立的,所以变量在他们各自的cache里面的顺序可能跟代码的顺序是不一致的,也就是说执行thread2的cpu可能会先看到变量b的变化,然后再看到变量a的变化,导致断言失败。就是我们常见的program order与process order的不一致的工程现象,这里就涉及到了memory consistency model的问题(类似于分布式系统的一致性)。
上述的代码如果要正确执行,则变量a、b之间需要有‘happen before’的语义来约束(这里就可以联想到分布式系统中因果一致性的概念)。但是对于这个语义上的需求,硬件设计者也爱莫能助,因为CPU无法知道变量之间的关联关系。所以硬件设计者提供了memory barrier指令,让软件可以通过这些指令来告诉CPU这类关系,实现program order与process order的顺序一致。类似于下面的代码:
-Thread 1-
void foo(void)
{
a = ;
memory_barrier();
b = ;
}
增加memory barrier之后,就可以保证在执行b=1的时候,cpu已经处理过'a=1'的操作了。也就是说通过硬件提供的memory barrier语义,使得软件能够保证其之前的内存访问操作先于其后的完成。memory barrier 常用的地方包括:实现内核的锁机制、应用层编写无锁代码、原子变量等。下面我们一起看下,c++11是怎样使用内存屏障来实现原子操作的。
C++11的原子操作
在C++11标准出来之前,C++标准没有一个明确的内存模型,各个C++编译器实现者各自为政,随着多线程开发的普及解决这个问题变得越来越迫切。在标准出来之前,GCC的实现是根据Intel的开发手册搞出的一系列的__sync原子操作函数集合,具体如下:
type __sync_fetch_and_OP (type *ptr, type value, ...)
type __sync_OP_and_fetch (type *ptr, type value, ...)
bool__sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...)
__sync_synchronize (...)
在C++11新标准中规定的内存模型(memory model)颗粒要比上述的内存模型细化很多,所以软件开发者就有很多的操作空间了,如果熟悉这些内存模型,在保证业务正确的同时可以将对性能的影响减弱到最低,在硬件资源吃紧的地方,这是我们优化程序的一个重要方向。
我们以c++11的原子变量的保证来展开这些内存模型。原子变量的通用接口使用store()和load()方式进行存取,可以额外接受一个额外的memory order参数,这个参数就是对应了c++11的内存模型,根据执行线程之间对变量的同步需求强度,新标准下的内存模型可以分成如下几类:
Sequentially Consistent
该模型是最强的同步模式,参数表示为std::memory_order_seq_cst,同时也是默认的模型。
-Thread -
y =
x.store (); -Thread2-
if(x.load() ==)
assert (y ==)
对于上面的例子,即使x和y是不相关的,通常情况下处理器或者编译器可能会对其访问进行重排,但是在seq_cst模式下,x.store(2)之前的所有memory accesses都发生在store操作之前。同时,x.load()之后的所有memory accesses都发生在load()操作之后,也就是说seq_cst模式下,内存的限制是双向的。
Acquire/Release Consistent
std::atomic<int> a{};
intb =;
-Thread -
b = ;
a.store(, memory_order_release);
-Thread -
while(a.load(memory_order_acquire) !=)/*waiting*/;
std::cout<< b <<'\n';
毫无疑问,如果是memory_order_seq_cst内存模型,那么上面的操作一定是成功的(打印变量b显示为1)。
1. memory_order_release保证在这个操作之前的memory accesses不会重排到这个操作之后去,但是这个操作之后的memory accesses可能会重排到这个操作之前去。通常这个主要是用于之前准备某些资源后,通过store+memory_order_release的方式”Release”给别的线程;
2. memory_order_acquire保证在这个操作之后的memory accesses不会重排到这个操作之前去,但是这个操作之前的memory accesses可能会重排到这个操作之后去。通常通过load+memory_order_acquire判断或者等待某个资源,一旦满足某个条件后就可以安全的“Acquire”消费这些资源了。
这个就是类似于分布式系统的因果一致性的概念。
Relaxed Consistent
这个是最宽松的模式,memory_order_relaxed没有happens-before的约束,编译器和处理器可以对memory access做任何的re-order,因此另外的线程不能对其做任何的假设,这种模式下能做的唯一保证,就是一旦线程读到了变量var的最新值,那么这个线程将再也见不到var修改之前的值了(这个类似于分布式系统单调读保证的概念)。
这种情况通常是在需要原子变量,但是不在线程间同步共享数据的时候会用,同时当relaxed存一个数据的时候,另外的线程将需要一个时间才能relaxed读到该值(也就是最终如果变量不再更改的话,所有的线程还是可以读取到变量最终的值的),在非缓存一致性的构架上需要刷新缓存。在开发的时候,如果你的上下文没有共享的变量需要在线程间同步,选用Relaxed就可以了。
这一点类似于分布式系统的最终一致性概念了。
总结
上述的过程体现的是强一致性、因果一致性、最终一致性等概念在c++11原子操作的使用,以及当前技术圈非常热门的话题分布式系统开发中分布式一致性概念的思考与迁移。从中我们可以看出技术在发展,但是很多概念其实是一脉相承的,只有深刻理解了概念背后的原理以及相关技术发展的背景,才能勉强跟上技术的发展浪潮。
从多核CPU Cache一致性的应用到分布式系统一致性的概念迁移的更多相关文章
- java并发编程(三)cpu cache & 缓存一致性
一 cpu cache 1. cache的意义 为什么需要CPU cache?因为CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源.所以cache的出 ...
- 读书笔记:7个示例科普CPU Cache
本文转自陈皓老师的个人博客酷壳:http://coolshell.cn/articles/10249.html 7个示例科普CPU Cache (感谢网友 @我的上铺叫路遥 翻译投稿) CPU cac ...
- <转>科普CPU Cache line
转载于http://coolshell.cn/articles/10249.html CPU cache一直是理解计算机体系架构的重要知识点,也是并发编程设计中的技术难点,而且相关参考资料如同过江之鲫 ...
- CPU指令重排序与MESI缓存一致性
一.重排序场景 class ResortDemo { int a = 0; boolean flag = false; public void writer() { a = 1; //1 flag = ...
- 从Java视角理解CPU缓存(CPU Cache)
从Java视角理解系统结构连载, 关注我的微博(链接)了解最新动态众所周知, CPU是计算机的大脑, 它负责执行程序的指令; 内存负责存数据, 包括程序自身数据. 同样大家都知道, 内存比CPU慢很多 ...
- 【转】多核CPU运行模式
多核CPU运行模式主要有以下三种: •非对称多处理(Asymmetric multiprocessing,AMP)——每个CPU内核运行一个独立的操作系统或同一操作系统的独立实例(instantiat ...
- (概念)多个CPU和多核CPU以及超线程(Hyper-Threading)
引言 在这篇文章中我会主要介绍CPU相关的一些重要概念和技术.如果你想更好地了解操作系统,那就从本文开始吧. 中央处理器(Central processing unit) 在我们了解其它概念之前,我们 ...
- [转帖]CPU Cache 机制以及 Cache miss
CPU Cache 机制以及 Cache miss https://www.cnblogs.com/jokerjason/p/10711022.html CPU体系结构之cache小结 1.What ...
- “多个单核CPU”与“单个多核CPU”哪种方式性能较强?
多个单核CPU: 成本更高,因为每个CPU都需要一定的线路电路支持,这样对主板上布局布线极为不便.并且当运行多线程任务时,多线程间通信协同合作也是一个问题.依赖总线的传输,速度较慢,且每一个线程因为运 ...
随机推荐
- Win32 COM组件 x Android Service (二)
继续上一篇. 如果不使用AIDL(Android Interface Definition Language接口描述语言)编写服务接口的话,(COM组件,CORBA组件,ICE组件以及其它远程调用框架 ...
- 结合RBAC模型讲解权限管理系统需求及表结构创建
在本号之前的文章中,已经为大家介绍了很多关于Spring Security的使用方法,也介绍了RBAC的基于角色权限控制模型.但是很多朋友虽然已经理解了RBAC控制模型,但是仍有很多的问题阻碍他们进一 ...
- 从壹开始 [ Ids4实战 ] 之六 ║ 统一角色管理(上)
前言 书接上文,咱们在上周,通过一篇<思考> 性质的文章,和很多小伙伴简单的讨论了下,如何统一同步处理角色的问题,众说纷纭,这个我一会儿会在下文详细说到,而且我最终也定稿方案了.所以今天咱 ...
- JavaScript笔记二
1.表格 - 在网页中可以通过表格来表示一些格式化的数据 - 表格相关的标签 - <table> 用来创建一个表格 - <tr> 表示表格中的一行 - <th> 表 ...
- 数据降维-PCA主成分分析
1.什么是PCA? PCA(Principal Component Analysis),即主成分分析方法,是一种使用最广泛的数据降维算法.PCA的主要思想是将n维特征映射到k维上,这k维是全新的正交特 ...
- Bash Shell编程简记
Shell编程简记 经常在linux环境下开发的同学,难免要使用shell的一些命令或者编写小的脚本,我这里也总结和整理下,自己对Shell的理解和常用的一些shell脚本. 按照目录分为如下3个节: ...
- SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder”
1.问题描述: 我的项目是tcServer,在运行的之后出现如下错误: SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBin ...
- ELK的简单搭建
Environment (都需要Java环境,jdk){ elasticsearch kibana 安装nginx用以测试 logstash } 1.首先拉取软件包,给予Java语言开发首选配置Ja ...
- 使用 SecureRandom 产生随机数采坑记录
公众号「码海」欢迎关注 背景 我们的项目工程里经常在每个函数需要用到 Random 的地方定义一下 Random 变量(如下) public void doSomethingCommon() { Ra ...
- Flask容器化部署原理与实现
本文将介绍Flask的部署方案:Flask + Nginx + uWSGI,并使用docker进行容器化部署,部署的实例来源 Flask开发初探,操作系统为ubuntu. Flask系列文章: Fla ...