1. 首页 > 快讯

深入了解原子操作及其应用

大家好,关于深入了解原子操作及其应用很多朋友都还不太明白,不过没关系,因为今天小编就来为大家分享关于的知识点,相信应该可以解决大家的一些困惑和问题,如果碰巧可以解决您的问题,还望关注下本站哦,希望对各位有所帮助!

前言所谓原子操作,就是要么没有,要么全部。在很多场景下,都需要原子操作。在翻看aep的spec文档时,我还发现了一个巧妙的方法。那么顺便我们发散性的总结一下原子操作的各种实现方法。欢迎大家交流讨论。

小粒度——指令

根据Intel手册第3卷第8章中的描述,x86使用三种机制来实现原子操作:

1.保证原子操作。保证原子操作是指一些基本的读写内存操作,保证是原子的。一般来说,读取和写入位于高速缓存行中的数据是原子的。

2. 总线锁定,使用LOCK#信号和指令的锁定前缀。锁定总线的方法非常简单。执行原子操作的CPU 将在总线上发出LOCK# 信号。这时,其他CPU的操作就会被阻塞。

3、缓存锁是利用缓存一致性协议(MESI协议)实现的。如果要访问的内存区域已经在当前CPU的缓存中,则使用缓存一致性协议来实现原子操作,否则总线将被锁定。

Intel早期的CPU(如Intel386、Intel486、Pentium处理器)通过总线锁来实现原子操作。这种实现的问题在于,两个完全不相关的CPU 也会相互竞争总线锁,从而导致整体性能下降。在后来的CPU中,Intel优化了这个问题。当用于原子操作的内存已经被拉入缓存时,CPU会使用缓存一致性协议来保证原子性,这称为缓存锁。与总线锁相比,缓存锁的粒度更细,可以获得更好的性能。

在x86中,有些指令内置了锁语义,例如XCHG、更新段描述符等;其他指令可以手动添加锁前缀来实现锁语义,例如BTS、BTR、CMPXCHG指令。这些指令中,最核心的就是CAS(Compare And Swap)指令,它是实现各种锁语义的核心指令。与XCHG有自己的原子语义不同,CAS操作必须以“lock CMPXCHG”的形式实现。一般来说,原子操作的数据长度不会超过8个字节,并且不允许同时对两个内存地址进行CAS操作(如果可以的话,无锁双向链表不是梦)。

原子操作中另一个不可避免的话题是ABA问题。由于本人水平有限,就不多讨论了。举个简单的例子,在Linux内核的slub实现中,使用了一个宏cmpxchg_double。这并不是同时对两个内存地址执行CAS的黑魔法,而是利用CMPXCHG16B指令解决ABA问题的宏函数。如果你有兴趣可以深入研究一下。

大粒度

当原子操作的对象大小在16字节或8字节以内时,一条或两条指令即可实现原子操作。然而,当对象的大小很大时,就需要其他方法来实现原子操作,例如锁定和COW。仔细观察这两个方法,我们可以发现,本质上,它们还是将问题转化为16字节的原子操作。

加锁锁定方法很容易理解。一旦锁被锁定,整个临界区操作就可以看做是一个原子操作。

内核提供了多种锁,自旋锁,读写锁,seq锁,互斥锁,信号量等,这些锁对于读者和写者有不同的偏好,在是否允许睡眠方面也有所不同。

简单来说,自旋锁和读写锁的核心都是通过CAS的原子指令来操作一个32位/64位的值。它们不允许睡眠,但读写锁针对读者进行了优化,并允许多个读者。数据同时读取,自旋锁对于读写操作没有优先权。 seq基于自旋锁实现,不允许休眠,但对编写者更加友好。互斥量和信号量也是基于自旋锁实现的,但是它们允许互斥量区域中的操作进入睡眠状态。

可见,这种加锁方式的核心就是利用指令来实现原子操作。

COW对大对象执行原子操作的另一种方法是COW(写时复制)。

牛的想法其实很简单。首先,我们有一个指向这个大对象的指针。当我们需要原子地修改这个大对象的数据时,因为没有办法就地修改,我们就复制这个对象的数据。修改对象副本,最后原子地修改指向该对象的指针。可以看到,这里的核心点就是用指令来代替指针。

关于COW,这里以AEP为例。 AEP是一种存储介质。这里你需要知道的是它可以按字节寻址,并且断电后数据不会消失。普通磁盘一般都会保证扇区的原子性,即如果在向某个扇区写入新数据时突然断电,则该扇区要么根本没有新数据,要么新数据已全部写入。不会出现半新半旧的状态。扇区原子性的保证非常重要,很多数据库都依赖它。但AEP等存储介质则没有这种保证,因此需要一种软件方法来保证这一点,称为BTT。

BTT的想法也很简单。为了方便理解,后面我就不介绍AEP的术语来描述了。

首先将整个存储空间划分为若干个块,每个块都有自己的物理块号,然后维护一个表将逻辑块号转换为物理块号。给予上层的逻辑块数量略小于物理块数量,因此会有一些物理块没有被映射,暂称为空闲块。

例如下图中,有4个逻辑块和5个物理块,其中1号块是空闲块。

接下来,向逻辑块写入数据时,首先找到一个空闲块,将数据写入那里,然后去映射表修改逻辑块到空闲块的映射。整个过程中,最关键的步骤——修改映射关系——是原子的。只要有这个保证,就可以提供原子更新区块数据的能力。

COW的思想在很多地方都可以找到,比如qemu的qcow镜像快照、ext4和btrfs写入数据时的cow、Linux内核的rcu机制等等。另外,cow最著名的使用场景是fork的实现,不过只是为了减少复制开销,与原子性关系不大。

COW优化cow方法一个很麻烦的事情就是每次都要原子更新指针。那么有没有办法去掉这个指针呢?是的。

这是我从Intel 关于AEP 的文档中学到的另一个技巧(注意,下面描述的示例与上面提到的BTT 无关)。原因是这样的:

AEP 驱动程序使用称为索引块的结构来管理元数据。该索引块位于整个媒体的开头,大小至少为256 字节。有些操作会改变多个字段的值,所以在改变字段的过程中可能会出现断电的情况。因此,需要一种机制来确保变更过程是原子的。

普通的COW方法需要两个索引块大小的空间,并在起始位置保留一个指针,并保留一个索引块作为备份。当修改索引块的数据时,将COW方法中的所有数据存储到备份索引块中,然后在COW方法中更改指针指向备份索引块。

Intel 使用以下机制来优化离开指针:

还有两个索引块,索引块中有一个字段叫seq。 seq 是一个两位数,共有4 个状态。除了00状态外,还有01、10、11三种状态,这三种状态视为一个循环,如下:

为了描述方便,将两个索引块分别命名为blockA和blockB。

第一次写入数据时,写入blockA,其seq为01;第二次写入数据时,写入blockB,其seq为10;第三次写入数据时,写入到blockA中,其seq为11;第四次向blockB写入数据时,其seq为01;以此类推,恢复时只需读取两个索引块并比较哪个seq在循环的最前面,就可以找到最新的索引堵塞。这样做的优点是显而易见的。首先,它避免了额外的指针,或者将指针固化为两个索引块,避免了8字节指针对齐两个索引块带来的麻烦;第二,节省了一次写操作,提高了效率。

多对象

前面的都是关于单个对象的。如果涉及多个对象,保证原子性会比较复杂。例如,如果使用add-and-unlock方式,需要注意锁的顺序,防止死锁问题;如果使用cow方法,需要注意失败后回滚被替换的指针的问题。从更大的角度来看,对多个对象的原子操作本质上是事务操作。所以,这个问题的解决办法,就参考事务的实现吧。

写日志

事务的四大特性ACID是原子性、一致性、隔离性和持久性。它们基本上都是常识,原子性只是事务的一个特性。

写日志是实现事务最常见的方式。日志一般分为重做日志和撤消日志。为了加快恢复速度,一般会引入检查点的概念。在文件系统和数据库的实现中,基本上都可以看到事务。

除了保证原子性和一致性之外,写入日志对于磁盘等外部存储设备也很友好,因为写入日志基本上是顺序的。这方面的典型案例是日志结构文件系统和leveldb的LSM-tree。

leveldb的原理就不用多说了。它将K-V对的增删改查操作一一变成日志,然后持久化到磁盘上的SST中,然后触发合并和排序。这样,基本上所有对磁盘的操作都是顺序的。

日志结构文件系统也有类似的想法。直接将文件数据的增删改查操作转化为日志写入磁盘。文件的实际数据不需要单独存储在某处,而是通过日志来恢复。这种方式对于写操作非常友好,但是读性能有点差强人意。

事务内存

事务通常用于保证数据的持久一致性。去掉持久化的要求,在内存对象的操作中引入事务的概念,就有了事务内存的概念。

上面提到,对于多个对象的操作,加锁和cow方法使用起来比较麻烦。加锁方法必须考虑解锁顺序,防止死锁。如果中途失败,必须按照特定顺序解锁并回滚;牛也是如此。虽然不存在死锁问题,但是回滚起来也很麻烦。还有一个问题是,针对不同的场景,添加和解锁的顺序需要重新考虑,cow的回滚也要重新考虑,不具有通用性。

事务内存机制就是为了解决这些问题而提出的。它将多个对象的原子操作抽象为一个事务,你只需要根据它提供的API以序列化的方式进行编程即可。无需考虑解锁或回滚的顺序。当遇到一些致命错误时,只需中止事务即可。这是一种通用的并发编程方法,在保证并发性能的同时,简化了编码。

事实上,事务内存机制的内部实现也依赖于奶牛机制和加密解锁。此外,它还依赖于原子操作指令。

总结

总结一下:

对于16字节或8字节以内的内存数据,使用CPU的原子操作指令;

对于超过16字节的数据,使用锁定、COW或者使用seq优化的COW方法,其本质上依赖于原子指令;

对于多个对象的原子操作,引入了事务或事务内存的概念。实际的实现要么是写日志,要么是依靠cow或者locking的方法,最终还是依靠原子指令。

因此,原子操作指令非常重要。

参考链接

https://pmem.io/documents/NVDIMM_Namespace_Spec.pdf

https://software.intel.com/content/dam/develop/public/us/en/documents/325462-sdm-vol-1-2abcd-3abcd.pdf

https://zhuanlan.zhihu.com/p/151425608

https://zh.wikipedia.org/wiki/%E8%BD%AF%E4%BB%B6%E4%BA%8B%E5%8A%A1%E5%86%85%E5%AD%98

OK,本文到此结束,希望对大家有所帮助。

用户评论

◆残留德花瓣

我一直很想知道,什么是原子操作?这篇文章讲的会不会比较简单易懂?

    有18位网友表示赞同!

相知相惜

我觉得很多程序设计基础知识都应该从原子操作开始学起啊!

    有12位网友表示赞同!

不忘初心

希望能介绍一些常见的原子操作,让我也能看看有没有应用在自己做的项目里。

    有17位网友表示赞同!

无寒

我对编程方面的东西不太了解,希望这篇文章能够解释得通俗易懂.

    有7位网友表示赞同!

残留の笑颜

现在很多软件都是很复杂的设计,不知道原子操作在这个架构设计中扮演着什么角色?

    有9位网友表示赞同!

身影

学习原子操作是不是会对提高代码的效率有帮助呢?

    有17位网友表示赞同!

那伤。眞美

我平时写程序的时候很少去考虑原子操作的问题,看来需要好好了解一下了!

    有5位网友表示赞同!

殃樾晨

这个标题很有吸引力,我想看看具体的例子能解释清楚原子操作的概念。

    有20位网友表示赞同!

荒野情趣

如果软件开发都离不开原子操作,那么掌握它是不是很关键?

    有13位网友表示赞同!

柠栀

现在数据并发处理很常见,不知道原子操作在这些领域应用情况如何?

    有17位网友表示赞同!

莫阑珊

看了这个标题,让我想到那些微服务架构设计中也涉及到原子操作吧?

    有11位网友表示赞同!

青衫负雪

期待看到一些实际的程序代码案例,能更直观地理解原子操作。

    有20位网友表示赞同!

别伤我i

学习编程一直很需要掌握一些基础的概念,这次好好看看原子操作有哪些特性呢!

    有17位网友表示赞同!

百合的盛世恋

感觉学习编程知识就是不断的去解决具体的问题,原子操作就是一种解决之道?

    有20位网友表示赞同!

枫无痕

这个标题让我联想到了银行转账这种操作,是不是也是一种原子操作?

    有19位网友表示赞同!

半梦半醒i

我平时学习计算机科学概念的时候,发现很多东西都是相互关联的,不知道原子操作和哪些相关知识联系在一起?

    有10位网友表示赞同!

孤自凉丶

我很期待看到一些关于不同编程语言中实现原子操作的方式介绍。

    有8位网友表示赞同!

あ浅浅の嘚僾

对于初学者来说,这篇文章能够帮助我们更好地理解原子操作的本质吗?

    有16位网友表示赞同!

余笙南吟

我相信通过学习原子操作,可以让我写出更安全、更高效的代码!

    有10位网友表示赞同!

本文采摘于网络,不代表本站立场,转载联系作者并注明出处:https://www.iotsj.com//kuaixun/7378.html

联系我们

在线咨询:点击这里给我发消息

微信号:666666