概述

现代多核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一致性的应用到分布式系统一致性的概念迁移的更多相关文章

  1. java并发编程(三)cpu cache & 缓存一致性

    一 cpu cache 1. cache的意义    为什么需要CPU cache?因为CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源.所以cache的出 ...

  2. 读书笔记:7个示例科普CPU Cache

    本文转自陈皓老师的个人博客酷壳:http://coolshell.cn/articles/10249.html 7个示例科普CPU Cache (感谢网友 @我的上铺叫路遥 翻译投稿) CPU cac ...

  3. <转>科普CPU Cache line

    转载于http://coolshell.cn/articles/10249.html CPU cache一直是理解计算机体系架构的重要知识点,也是并发编程设计中的技术难点,而且相关参考资料如同过江之鲫 ...

  4. CPU指令重排序与MESI缓存一致性

    一.重排序场景 class ResortDemo { int a = 0; boolean flag = false; public void writer() { a = 1; //1 flag = ...

  5. 从Java视角理解CPU缓存(CPU Cache)

    从Java视角理解系统结构连载, 关注我的微博(链接)了解最新动态众所周知, CPU是计算机的大脑, 它负责执行程序的指令; 内存负责存数据, 包括程序自身数据. 同样大家都知道, 内存比CPU慢很多 ...

  6. 【转】多核CPU运行模式

    多核CPU运行模式主要有以下三种: •非对称多处理(Asymmetric multiprocessing,AMP)——每个CPU内核运行一个独立的操作系统或同一操作系统的独立实例(instantiat ...

  7. (概念)多个CPU和多核CPU以及超线程(Hyper-Threading)

    引言 在这篇文章中我会主要介绍CPU相关的一些重要概念和技术.如果你想更好地了解操作系统,那就从本文开始吧. 中央处理器(Central processing unit) 在我们了解其它概念之前,我们 ...

  8. [转帖]CPU Cache 机制以及 Cache miss

    CPU Cache 机制以及 Cache miss https://www.cnblogs.com/jokerjason/p/10711022.html CPU体系结构之cache小结 1.What ...

  9. “多个单核CPU”与“单个多核CPU”哪种方式性能较强?

    多个单核CPU: 成本更高,因为每个CPU都需要一定的线路电路支持,这样对主板上布局布线极为不便.并且当运行多线程任务时,多线程间通信协同合作也是一个问题.依赖总线的传输,速度较慢,且每一个线程因为运 ...

随机推荐

  1. 反汇编分析NSString,你印象中的NSString是这样吗

    我们先来定义三个NSString -(void) testNSString { NSString* a = @"abc"; NSString* b = [NSString stri ...

  2. PostgreSQL各数据类型的内置函数

    参考<PostgreSQL实战> 3.1.2 数字类型操作符和数学函数 PostgreSQL 支持数字类型操作符和丰富的数学函数 例如支持加.减.乘.除.模取取余操作符 SELECT 1+ ...

  3. C#读写XML的两种一般方式

    针对XML文档的应用编程接口中,一般有两种模型:W3C制定的DOM(Document Object Method,文档对象模型)和流模型. 流模型的两种变体:"推"模型(XML的简 ...

  4. Linux\Nginx 虚拟域名配置及测试验证

    使用 Nginx 虚拟域名配置,可以不用去购买域名,就可以通过特定的域名访问本地服务器.减少发布前不必要的开支. 配置步骤 1. 编辑 nginx.conf 配置文件 sudo vim /usr/lo ...

  5. PostGIS 结合Openlayers以及Geoserver实现最短路径分析(二)

    前文讲述了怎么用ArcMap制作了测试数据,并导入了PostGIS,接下来我们需要结合PgRouting插件,对入库的数据再进行一下处理. 1.在pgAdmin中,执行下面的sql语句 --添加起点字 ...

  6. 扛把子组20191121-10 Scrum立会报告+燃尽图 06

    此作业的要求参见http://edu.cnblogs.com/campus/nenu/2019fall/homework/10070 一.小组情况: 队名:扛把子 组长:孙晓宇 组员:刘信鹏 韩昊 宋 ...

  7. 工作常用4种Java线程锁的特点,性能比较、使用场景

    多线程的缘由 在出现了进程之后,操作系统的性能得到了大大的提升.虽然进程的出现解决了操作系统的并发问题,但是人们仍然不满足,人们逐渐对实时性有了要求. 使用多线程的理由之一是和进程相比,它是一种非常花 ...

  8. 使用原生javaScript绘制带图片的二维码---js

    使用链接生成二维码主要是使用qr.js或者其他,把链接转化为二维码的形式,在使用canvas时需要设置画布的尺寸,生成的颜色. <div class="qr_code"> ...

  9. Python 并发总结,多线程,多进程,异步IO

    1 测量函数运行时间 import time def profile(func): def wrapper(*args, **kwargs): import time start = time.tim ...

  10. python3基础之 字符串切片

    一.python3中,可迭代对象有:列表.元组.字典.字符串:常结合for循环使用:均可使用索引切片 实例: str = ' #str[start:stop:step] 遵循[左闭右开]规则 prin ...