概述

现代多核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. spring security进阶 使用数据库中的账户和密码认证

    目录 spring security 使用数据库中的账户和密码认证 一.原理分析 二.代码实现 1.新建一个javaWeb工程 2.用户认证的实现 3.测试 三.总结 spring security ...

  2. 50.Qt-QJsonDocument读写json

    QJsonDocument: 提供一种读取和写入JSON文档的方法,可以通过它的的成员函数array()或object()检索文档中包含的数组或对象,然后读取JSON数据,或者修改数据. QJsonO ...

  3. DNS name

    DNS name 指的反向解析的域名.

  4. 4sql

    在 MySQL 中,有三种主要的类型:文本.数字和日期/时间类型. Text 类型:CHAR(size)VARCHAR(size)TINYTEXTTEXT 存放最大长度为 65,535 个字符的字符串 ...

  5. 个人收藏--未整理—C# 上传下载文件

    Winform下载文件 /// <summary> /// 下载文件 /// </summary> /// <param name="URL"> ...

  6. centos6.7下安装glibc-2.17

    glibc  所有版本下载地址 : http://ftp.gnu.org/pub/gnu/glibc/ 安装先决条件: #yum install gcc libffi-devel python-dev ...

  7. 【Android - 问题解决】之ScrollView嵌套ListView时总是自动滑动到ListView顶部的问题

    最近做了一个项目,里面有一个ScrollView嵌套ListView的布局. 做出来之后发现,进入这个界面之后,总是自动滑动到ListView的顶部,而ScrollView中位于ListView上面的 ...

  8. mysql客户端 navicat 本地导入sql文件出错

    以前遇到过这个问题,找了半天度娘没解决,然后就放弃了. 因为是自己 demo 的项目 所以就自己手动建表了. 现在实习了,去到公司下载下代码来,拿上sql 导入发现还是报错, 根本没法整,然后自己都不 ...

  9. Spring Boot SpringApplication启动类(一)

    目录 目录 前言 1.起源 2.SpringApplication 准备阶段 2.1.推断 Web 应用类型 2.2.加载应用上下文初始器 ApplicationContextInitializer ...

  10. C#中的委托和事件(二)

    引言 如果你看过了 C#中的委托和事件 一文,我想你对委托和事件已经有了一个基本的认识.但那些远不是委托和事件的全部内容,还有很多的地方没有涉及.本文将讨论委托和事件一些更为细节的问题,包括一些大家常 ...