MapReduce模型

序言

对于mapreduce的兴趣起源于我最近在学习javascript时遇到了map和reduce函数。使用这两个函数进行数据处理时,总感觉思路有一点奇特,理解起来需要绕一个弯子。之前在学python时,也看到过这两个函数,但是当时没有在意,现在我觉得我有必要对这两个函数做一个深入的研究了。

什么是MapReduce

MapReduce的深层概念远比两个函数来得复杂,它是一个编程模型,主要用于解决大数据处理问题。使用这种模式编写的程序是可并行的(parallelized)并适用于大型计算机集群。运行时由系统自动对输入数据进行分割、协调多机器间的计算任务、进行异常处理、管理机器间的内部通信。这样,没有并行计算和分布式系统相关经验的开发人员也能够借助大型分布式系统进行数据处理了。

百度百科中给出的定义比较全面,而且很准确。

MapReduce是面向大数据并行处理的计算模型、框架和平台,它隐含了以下三层含义:
1)MapReduce是一个基于集群的高性能并行计算平台(Cluster Infrastructure)。它允许用市场上普通的商用服务器构成一个包含数十、数百至数千个节点的分布和并行计算集群。
2)MapReduce是一个并行计算与运行软件框架(Software Framework)。它提供了一个庞大但设计精良的并行计算软件框架,能自动完成计算任务的并行化处理,自动划分计算数据和计算任务,在集群节点上自动分配和执行任务以及收集计算结果,将数据分布存储、数据通信、容错处理等并行计算涉及到的很多系统底层的复杂细节交由系统负责处理,大大减少了软件开发人员的负担。
3)MapReduce是一个并行程序设计模型与方法(Programming Model & Methodology)。它借助于函数式程序设计语言Lisp的设计思想,提供了一种简便的并行程序设计方法,用Map和Reduce两个函数编程实现基本的并行计算任务,提供了抽象的操作和并行编程接口,以简单方便地完成大规模数据的编程和计算处理。

map和reduce

从编程语言的角度上来说
map操作会接收到两个参数,一个列表和一个变换过程,其功能是将这个变换过程映射到每一个列表成员上,从而得到一个新的列表。
reduce操作也会接收到两个参数,一个列表和一个变换过程,其功能是将这个变换过程从前向后作用在列表成员上,最终得到一个列表中条目。

从分布式系统的角度上来说 (以下内容摘自百度百科)

简单说来,一个映射函数就是对一些独立元素组成的概念上的列表(例如,一个测试成绩的列表)的每一个元素进行指定的操作(比如前面的例子里,有人发现所有学生的成绩都被高估了一分,它可以定义一个“减一”的映射函数,用来修正这个错误。)。事实上,每个元素都是被独立操作的,而原始列表没有被更改,因为这里创建了一个新的列表来保存新的答案。这就是说,Map操作是可以高度并行的,这对高性能要求的应用以及并行计算领域的需求非常有用。
而化简操作指的是对一个列表的元素进行适当的合并(继续看前面的例子,如果有人想知道班级的平均分该怎么做?它可以定义一个化简函数,通过让列表中的元素跟自己的相邻的元素相加的方式把列表减半,如此递归运算直到列表只剩下一个元素,然后用这个元素除以人数,就得到了平均分。)。虽然他不如映射函数那么并行,但是因为化简总是有一个简单的答案,大规模的运算相对独立,所以化简函数在高度并行环境下也很有用。

我之前在学习语言层面的map和reduce时,很难理解其用途,结合分布式计算的背景知识,就能够较形象地理解map和reduce操作的过程和作用。

并行计算与大数据

mapreduce模型给并行计算带来了革命性的影响,它是目前为止最成功、最广为接受和最易于使用的大数据并行处理技术。

mapreduce最初由google提出,并开发出了如下产品

  • Google File System(大规模分散文件系统)
  • MapReduce (大规模分散FrameWork)
  • BigTable(大规模分散数据库)
  • Chubby(分散锁服务)

随后就有其对应的开源实现,也就是Hadoop。Hadoop是Apache软件基金会发起的一个项目,它是一种分布式数据和计算的框架,它包含许如下子项目。

  • HDFS,hadoop distributed file system,是Google File System的开源实现
  • MapReduce,是Google MapReduce的开源实现
  • HBASE,类似Google BigTable的分布式NoSQL数据库
  • Zookeeper,分布式锁服务,类似Google Chubby

css flexbox 总结

引言

本文主要对css flexbox的关键点了总结,方便以后在以后的查阅。

容器的属性

对于容器需要指定其显示方式为flexbox

display: flex;

指定flex排列的方向、在排列时是否会换行,使用 flex-flow 可以快速设置二者属性

flex-direction: row | row-reverse | column | column-reverse;
flex-wrap: nowrap | wrap | wrap-reverse;
flex-flow: <'flex-direction'> || <'flex-wrap'>;

用于调整主轴方向的排布(对于 row 来说就是横向,对于 column 来说就是纵向)

justify-content: flex-start | flex-end | center
	       | space-between | space-around | space-evenly;

用于调整相交轴方向的行间排布(对于 row 来说就是纵向,对于 column 来说就是横向)

align-content: flex-start | flex-end | center
	     | stretch | space-between | space-around;

用于调整相交交轴方向的单行对齐方式。需要注意的是其中 centerbaseline 的区别: 二者都表示居中,而 baseline 会保证所有文字的底边处在同一条线上。

align-items: flex-start | flex-end | center | baseline | stretch;

条目的属性

用于调整顺序

order: <integer>; /* default 0 */

用于调整每个条目的伸展程度

flex-grow: <number>; /* default 0 */

用于调整每个条目的缩小程度

flex-shrink: <number>; /* default 1 */

用于调整每个条目的默认尺寸

flex-basis: <length> | auto; /* default auto */

设置flex属性,排列顺序为 flex-grow , flex-shrink , flex-basis

flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ];

用于重载容器的 align-items 的设置

align-self: auto | flex-start | flex-end | center | baseline | stretch;

emacs的键盘宏(keyboard macro)

对于一些有规律且重复性的编辑任务, 手动完成十分无聊, 并且需要耗费较长的时间。我在youtube上看到一个使用keyboard marco的 视频 后, 受到很大的启发, 在以后的使用中也会尝试使用宏。我总结了一下视频中的技巧要点,并查阅资料对相关知识点进行了补充和完善。

有梯子的同学可以去看看,视频地址:https://youtu.be/wFCO__0prCM

操作指令
开始记录宏: 命令名称 kmacro-start-macro , 快捷键 C-x-(<f3>
结束记录宏: 命令名称 kmacro-end-macro , 快捷键 C-x-)<f4>
执行宏: 命令名称 kmacro-end-and-call-macro , 快捷键 C-x-e , 可以使用 C-u 指定这个宏的执行次数
清除多余的空格: 命令名称 fixup-whitespace , 这个命令我是第一次见到, 以后可以尝试多用用。

参考资料

提高emacs中浏览和选择操作效率的技巧

1 引言

Gaurab Paul的 一篇博文 给了我很大的启发,他详细地介绍了emacs中的相关概念,并提供了许多充满想象力的小技巧。作为emacs的入门级选手确实学到了很多,也拓宽了自己的思路。

我最初的开发环境是Visual Studio,这一类比较大型的IDE集成了许多功能,但同时也会束缚住使用者的想法。通过这篇文章我感受到的由普通操作指令能组合成的新编辑方式。

如果英文水平允许的话,非常推荐阅读一下原版的博文,原文中有更加丰富形象的图片示例,无论是跟我一样刚刚入门emacs的新手,还是经验丰富的老兵,都能够从中获得启发。下面,我结合自己的理解和收获谈谈emacs中操作的体会。

2 pointmarkregion 的概念

我之前进行代码段复制的操作是十分基础的,用 [email protected] 模拟鼠标按下,方向键模拟鼠标拖动,在鼠标拖动的过程中就形成了一个选区,然后用 M-w 对这个选区进行复制操作,用 C-y 粘贴被复制的内容。

以上操作带出了几个非常重要的概念 。在emacs中,鼠标光标所在位置被称作 point ;组合键 [email protected] 执行的是 set-mark-command 命令,就是将 point 所在位置标记为 mark ;通过移动光标,也就是移动 point 后,在 pointmark 之间就形成了 region

3 region 操作技巧

3.1 调整 region 的大小

下面就来介绍一个非常重要的命令 exchange-point-and-mark ,这个命令默认被绑定在组合键 C-x C-x 上,从字面意思上很容易理解这条指令的作用,就是交换 markpoint 的位置。这样做的意义在于能够方便地切换 region 的可动边界,这样能够使 region 方便地分别从两端调整大小。
下面的示例是截取自Paul的博文,需要注意的是,他习惯于使用 C-SPC 调用 set-mark-command

Lorem ipsum dolor sit amet
      ^ Cursor


      Point
      |
      Mark
      |
Lorem ipsum dolor sit amet
      ^ C-spc


      Mark          Point
      | ----region--|
      |             |
Lorem ipsum dolor sit amet
	 move       ^
	 forward ->


      Mark          Point
      | ----region--|
      |             |
Lorem ipsum dolor sit amet
		    ^
		    C-x C-x


      Point         Mark
      | ----region--|
      |             |
Lorem ipsum dolor sit amet

Point and mark interchanged

3.2 使用 region 进行重复性输入

对于 region 相关的操作,通常是对已经存在的代码段进行编辑的,如果我们在输入之前就知道有许多字段是需要重复输入的,那么就可以在输入之前设置好 mark ,对输入后形成的 region 完成复制。这个技巧在特定情况能够很大地提升输入效率,但是我个人认为,想要在实战中完成这个操作,还需要保证非常清晰的思路。可以通过下面的示例感受这种操作带来的方便(示例截取自Paul的博文)。

nil

下面给出了详细的操作解析

class
      ^ C-spc => Activate mark

class Foo
	  ^ M-w => Foo has now been killed (copied)

class Foo extends React.Component<
				  ^ C-spc => Activate mark

class Foo extends React.Component<
				  ^ C-y => Yank (paste) Foo

class Foo extends React.Component<FooProps
					  ^ M-w => FooProps has now been killed (copied)

class Foo extends React.Component<FooProps>

// Later
interface
	   ^ C-y => Yank FooProps

interface FooProps {}

3.3 框选一个矩形的 region

使用 rectangle-mark-mode 命令,默认快捷键 C-x-SPC ,能够框选出一个矩形的 region 。对于矩形 region ,Paul给出的示例是复制 dired 中的多个文件名称,貌似其他合适的使用场景不太多。

nil

4 其他插件支持

有些插件拓展能够实现光标的快速定位,如 helm-swoopavy

4.1 helm swoop

从我个人的使用体验来看 helm-swoop 和helm occur的功能十分相似,它们都提供了方便的关键词跳转功能。

下面的图片来自helm swoop的 主页

nil

4.2 avy

avy 的思路非常独特,这样的跳转和定位让我想起了Chrome浏览器中的Vimium插件,他允许我们使用更少的按键就能跳转到当前buffer中的任意位置,略微遗憾的是它只支持拉丁字母,不过在编写代码的大多数情况下是够用的。

下面的图片来自Paul的博文

nil

5 参考资料

shell的输出重定向

引言

在linux中借助shell等命令行工具能够很方便地与操作系统交互,可以在shell中将命令或程序的输入结果重定向到特定地方,很方便地实现一些功能。这个技巧十分实用,使用输出重定向能够极大地简化我们的日常操作。

使用尖括号完成重定向

示例如下,运行下面的命令能够把 ls 命令的运行结果写入到 ls-output.txt 中。使用 > 会把程序运行时本该输出到 stdout 的内容重定向到指定名称的文件中。

ls . > ls-output.txt

可以在 > 左面写上数字和 & ,用以标识在重定向时的特殊用法。下面会给出一些特殊用法的实例。

重定向 stdout 到指定文件中

ls . 1> ls-output.txt

重定向 stderr 到指定文件中

ls . 2> ls-error.txt

stdoutstdout 合并再重定向到指定文件中

ls . 2>&1 ls-output-and-error.txt

以下命令具有相同的效果

ls . &> ls-output-and-error.txt

将程序的输出丢掉

可以将输出重定向到一个特殊的文件 /dev/null ,所有写入到这个文件的内容都会被丢弃掉。

program > /dev/null

将输出追加到指定文件尾部

使用一个尖括号( > )能够将输出重定向到文件中,在写入文件时会覆盖掉其中的内容。如果想保留文件中的原始内容,则可以用两个尖括号( >> ),这样就能将输出追加到文件的尾部。示例代码如下:

echo test >> file-output.txt

使用管道完成重定向

使用管道符号 | 能够将一个程序的输出重定向到另一个程序的输入中去。下面的命令会将 ls 的输出( stdout )重定向到 grep 的输入( stdin )中去。管道命令在linux中是最常见的用法。

ls | grep <pattern>

libshmcache源码阅读笔记

引言

由于在工作中需要开发一套内存缓存服务,使用了共享内存作为多进程间的数据共享。为了提高共享内存缓存服务的性能,我找了一个类似的较为成熟的开源项目 libshmcache ,通过研究源码学习其中的优点并改进自己的模块。

libshmcache与redis相似的是都使用内存进行数据缓存;与redis不同的是,redis使用的进程自己申请的动态内存,而libshmcache使用的是共享内存。使用共享内存就意味着libshmcache主要的应用场景是同一台主机上的数据缓存。

我花了一周时间阅读了比较感兴趣的部分代码,收获不少,现就以下几个方面总结一下自己的心得:

  • 纯C语言开发的代码风格
  • hash table的原理和实现
  • gcc原子化操作接口
  • 有锁写和无锁读的实现细节
  • 共享内存的两套函数接口(POSIX和SystemV)

纯C语言开发时的代码风格

我在工作中使用比较多的开发语言是C++,对于C语言编写的这样规模的项目,还是第一次仔细深入地研究。C语言使用 struct 作为大多数自定义数据结构的关键字,相对于C++能够使用成员函数能够对类进行功能拓展,C语言比较常用的是将这个对象作为输入参数传到函数中。

纵观所有项目代码,我感受比较深的就是使用结构体中嵌套匿名结构体,这样做能够增强数据结构的层次感,示例代码如下:

struct shmcache_context {
    pid_t pid;
    int lock_fd;    //for file lock
    int detect_deadlock_clocks;
    struct shmcache_config config;
    struct shm_memory_info *memory;
    struct {
	struct shmcache_segment_info hashtable;
	struct {
	    int count;
	    struct shmcache_segment_info *items;
	} values;
    } segments;

    struct shmcache_value_allocator_context value_allocator;
    struct shmcache_list list;   //for value recycle
    bool create_segment;  //if check segment size                                  
};

注意 shmcache_context 中的匿名结构体 segmentsvalues ,这样的写法体现了相互包含关系,也使后续的操作该数据结构的语句更加容易理解。

另外对于联合体和位域这两种技术也是我在之前开发中使用比较少的,通过阅读源码能够让我对其有了更深刻的理解。示例代码如下:

union shm_hentry_offset {
    int64_t offset;
    struct {
	int index :16;
	int64_t offset :48;
    } segment;
};

这段代码使用了联合体赋予了 shm_hentry_offset 两种访问方式,又使用了位域将 int64_t 分割为两段。

hash table的原理和实现

libshmcache内部使用的是hash table做内部缓存的数据结构,这使查找的时间复杂度是O(1)。
之前看过一些介绍hash table的资料,对hash table的工作原理是有过一个基础的了解的,这次通过阅读源码,能够了解到hash table在代码实现上更加细节的内容。
对于hash计算中出现的hash值冲突,即在hash计算时出现了两个不同的key在经过hash计算后得到的bucket相同,libshmcache采用的解决方案是使用linked list来存放这些相同bucket对应的value。

gcc原子化操作接口

使用原子化操作接口能够解决一些并发读写问题,原子化操作相对于互斥锁执行更快。原子化操作也是一种无锁编程的方式。

有锁写和无锁读的实现

在libshmcache中,写操作通过 pthread_mutex_t 进行同步,而读操作是无锁的。
对于写操作来说,需要对hash table进行操作,这肯定是需要同步的。
pthread_mutex_t 保存在共享内存中,不同的进程通过映射共享内存就能获得同一个互斥量,通过这个互斥量就能完成进程间同步。

共享内存的两套函数接口(POSIX和SystemV)

在linux上使用共享内存时有两套接口 mmapshmgetmmapPOSIX 标准的接口,而 shmgetSystem V 标准的接口,两者都能够实现进程间共享内存,但他们在使用上还是有些区别的。对于 mmap 来说,需要在硬盘上创建一个文件,再将该文件映射到内存中。对于 shmget 来说,需要指定一个key,不同的进程通过相同的key就能映射到同一片内存。

C/C++中的位域

1 什么是位域

维基百科 给出了以下解释

位域(或称“位段”,bit field)为一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。

cppreference 给出了以下定义

Declares a class data member with explicit size, in bits. Adjacent bit field members may be packed to share and straddle the individual bytes.

这种数据结构的好处:

  • 可以使数据单元节省储存空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。
  • 位段可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。

而位域这种数据结构的缺点在于,其内存分配与内存对齐的实现方式依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的。

2 位域的语法

identifier(optional) attr(optional) : size

注意:

  • size的大小不能超过identifier所包含最大比特位个数。
  • identifier为空时表示对应的size个数的比特位不使用
  • size为0时表示根据前类型强制补齐
struct S1 {
    // will usually occupy 2 bytes:
    // 3 bits: value of b1
    // 2 bits: unused
    // 6 bits: value of b2
    // 2 bits: value of b3
    // 3 bits: unused
    unsigned char b1 : 3, : 2, b2 : 6, b3 : 2;
};

struct S2 {
    // will usually occupy 2 bytes:
    // 3 bits: value of b1
    // 5 bits: unused
    // 6 bits: value of b2
    // 2 bits: value of b3
    unsigned char b1 : 3;
    unsigned char :0; // start a new byte
    unsigned char b2 : 6;
    unsigned char b3 : 2;
};

2.1 在size取0时如何理解

对于size取0时的各种情况进行了尝试,详细用例如下。思路是使用联合体能比较方便地将内存分布表示出来。
如下所示,定义了如下几种情况。

2.1.1 case1

union un1 {
    int n1;
    struct {
	int nn1 : 4;
	int : 0;
	int nn2 : 16;
    } st;
};

对联合体中的结构体位段进行赋值,并将联合体中的内容打印出来。

union un1 u1;
u1.st.nn1 = 1;
u1.st.nn2 = 1;
printf("union u1.n1=0x%08x sizeof(un1)=%zu\n", u1.n1, sizeof(un1));

输出结果为

union un1.n1=0x00000001 sizeof(un1)=8

2.1.2 case2

union un1 {
    int n1;
    struct {
	int nn1 : 4;
	char : 0;
	int nn2 : 16;
    } st;
};

输出结果为

union un1.n1=0x00000101 sizeof(un1)=4

2.1.3 case3

union un1 {
    int n1;
    struct {
	int nn1 : 4;
	short : 0;
	int nn2 : 16;
    } st;
};

输出结果为

union un1.n1=0x00010001 sizeof(un1)=4

2.1.4 case4

union un1 {
    int n1;
    struct {
	int nn1 : 8;
	char : 0;
	int nn2 : 16;
    } st;
};

输出结果为

union un1.n1=0x00000101 sizeof(un1)=4

2.2 struct 标识符

多数例子都是以 struct 作为位域的组织标识,在C++中能否使用 class 作为位域的标识符。
经过测试,是可以使用 class 的,但是需要注意 class 的默认访问控制属性为 private

3 位域的常见应用场景

为什么要使用位域?位域适合那些情况?
位域的主要使用目的是节省对象的内存使用。在存放一些比较小的数据时,使用位域能够使字节中的每个比特位合理地利用起来,避免内存浪费。
比较典型的应用是描述硬件寄存器。如果有32个一组的寄存器,每个寄存器代表一个比特位,就可以使用位域表示这组寄存器。

4 C++中的位操作接口

C++中也提供了一套位操作的接口 std::bitset ,这套接口提供了指定比特位数据的操作接口。

GCC的内存原子化操作函数接口

1 原子化操作

在并发编程中,一个操作或一组操作是原子操作、可线性化操作、不可分操作或不可中断操作(atomic, linearizable, indivisible, uniterruptible),表示该操作执行时不可被中断的。操作的原子性能够保证操作在执行时免受中断、信号、并发进程线程的影响。另外,原子操作大多只有两种结果,要么成功并改变系统中对应的状态,要么没有相关效果。

原子化经常由互斥来保证,可以在硬件层面建立一个缓存一致性协议,也可以在软件层面使用信号量或加锁。因此,一个原子操作不是必须实际上马上生效,而操作系统让这个操作看起来是直接发生的,这能够让操作系统保持一致。正是如此,只要不影响性能,用户可以忽略较底层的实现细节。

2 函数接口

GCC提供了原子化的操作接口,能够支持长度为1、2、4、8字节的整形变量或指针。

In most cases, these builtins are considered a full barrier. That is, no memory operand will be moved across the operation, either forward or backward. Further, instructions will be issued as necessary to prevent the processor from speculating loads across the operation and from queuing stores after the operation.

在大多数情况下,这些内建函数是完全内存栅栏(full barrier)的,以上摘自 GCC Manual。

取值并进行对应操作的接口 如下所示:

type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)

这些函数接口的执行逻辑如下:会执行名称相对应的运算,并将内存中之前存放的值取出并返回。

{ tmp = *ptr; *ptr op= value; return tmp; }
{ tmp = *ptr; *ptr = ~(tmp & value); return tmp; }   // nand

需要注意的是 :从GCC 4.4开始 __sync_fetch_and_nand 是按照 *ptr = ~(*ptr & value) 实现的,而不是 *ptr = ~*ptr & value

直接操作并返回结果的接口 如下所示:

type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...)

这些函数接口的执行逻辑如下:

{ *ptr op= value; return *ptr; }
{ *ptr = ~(*ptr & value); return *ptr; }   // nand

需要注意的是 :从GCC 4.4开始 __sync_nand_and_fetch 是按照 *ptr = ~(*ptr & value) 实现的,而不是 *ptr = ~*ptr & 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, ...)

3 内存栅栏(Memory Barrier)

在上面的说明中提到了memory barrier的概念,这个概念是CPU指令的一个术语。

内存栅栏又叫内存屏障,是一种能够让CPU或编译器约束内存操作指令执行顺序的屏蔽指令。这表示在内存栅栏前的指令能够保证执行时先于内存栅栏后的指令。由于大多数现代CPU采用性能优化会导致指令执行变序时,所以内存栅栏是十分必要的。这样的指令变序对于单线程程序一般不会有很大影响,但是在并发编程情况下如果不加以控制就会导致不可预知的结果。

内存栅栏的典型应用场景就是用于实现多设备之间的共享内存的底层机器码。这些代码包括原始同步机制、多核系统上的无锁数据结构、与计算机硬件交互的设备驱动。

内存栅栏对于无锁编程来说十分重要的。

内存栅栏与volatile关键字
内存栅栏分为读栅栏(read barrier)、写栅栏(write barrier)、获取栅栏(acquire barrier)、释放栅栏(release barrier)等。内存栅栏并不能保证数值的是“最新的”或“新鲜的”,它只能控制内存访问的相对顺序。

“写栅栏”用于控制写操作的顺序。由于相对于CPU的执行速度来说,向内存中写入顺序是比较慢的,通常会有一个写入请求队列,所以实际的写入操作发生在指令发起之后,队列中指令的顺序可能会被重新排序。写栅栏能够防止指令变序。

“读栅栏”用于控制读操作的顺序。由于预先执行(CPU会提前将内存中的数据读回来),并且CPU有缓存区(CPU会从缓存中而不是内存中读取数据),读操作可能会出现变序。

volatile关键字值能通知编译器生成的输出码从内存中重新读取数据,但是不会告诉CPU在如何读取数据、在哪里读取数据。

“获取栅栏”能够保证特定指令块之前的执行顺序。例如获取读,在向读队列中加入读操作,“获取栅栏”意味着在这条操作之后可以出现指令变序,而这条操作之前不会出现指令变序。

“释放栅栏”能够保证特定指令块之后的执行顺序。例如释放写,在向写队列中加入写操作,“释放栅栏”意味着在这条写操作之前的指令不会变序到该指令之后,而这条该操作的之后的指令可能会变序到该指令之前。

获取栅栏和释放栅栏是又叫半栅栏(half barrier),这是因为它们只能防止单方向的指令变序。

4 操作原子化能够解决多进程访问共享内存的问题吗?

原子化操作是对于CPU而言的指令操作,它不关心线程还是进程,它只关心这一系列的指令是不可分割的。所以,进程间可以使用原子操作完成内存的操作同步。