当前位置:首页 > 芯闻号 > 充电吧
[导读]AtomicPointer 是 leveldb 提供的一个原子指针操作类,使用了基于原子操作(atomic operation)或者内存屏障(memory barrier)的同步访问机制,这比用锁和信

AtomicPointer 是 leveldb 提供的一个原子指针操作类,使用了基于原子操作(atomic operation)或者内存屏障(memory barrier)的同步访问机制,这比用锁和信号量的效率要高。

一.Windows版本的AtomicPointer实现

先上源码,这个是Windows版本的源码,源文件位置:leveldb/port/port_win.h和leveldb/port/port_win.cc

class AtomicPointer {
 private:
  void * rep_;
 public:
  AtomicPointer() : rep_(nullptr) { }
  explicit AtomicPointer(void* v); 
  void* Acquire_Load() const;
  void Release_Store(void* v);
  void* NoBarrier_Load() const;
  void NoBarrier_Store(void* v);
};
AtomicPointer::AtomicPointer(void* v) {
  Release_Store(v);
}
// 使用原子操作的方式读取,即同步的读操作
void* AtomicPointer::Acquire_Load() const {
  void * p = nullptr;
  InterlockedExchangePointer(&p, rep_);
  return p;
}
// 使用原子操作的方式写入,即同步的写操作
void AtomicPointer::Release_Store(void* v) {
  InterlockedExchangePointer(&rep_, v);
}
// 不使用原子操作的方式读取,即不同步的读操作
void* AtomicPointer::NoBarrier_Load() const {
  return rep_;
}
// 不使用原子操作的方式写入,即不同步的写操作
void AtomicPointer::NoBarrier_Store(void* v) {
  rep_ = v;
}
从代码中可以看出,AtomicPointer是基于原子操作实现的一个原子指针操作类,通过原子操作实现多线程的读写同步。原子操作,即不可分割开的操作。该操作一定是在同一个CPU时间片中完成,这样即使线程被切换,多个线程也不会看到同一块内存中不完整的数据。

这里同步没有用到锁,所以涉及到了无锁编程(Lock-Free)的概念。

二.无锁编程

无锁编程具体使用和考虑到的技术方法包括:原子操作(atomic operation)、内存栅栏(memory barrier)、内存顺序冲突(memory order)、 指令序列一致性(sequential consistency)等等。之所以会出现无锁编程技术,因为基于锁的编程的有如下缺点。
多线程编程是多CPU(多核CPU或者多个CPU)系统在中应用最广泛的一种编程方式,在传统的多线程编程中,多线程之间一般用各种锁的机制来保证正确的对共享资源(share resources)进行访问和操作。在多线程编程中只要需要共享某些数据,就应当将对它的访问串行化。比如像++count(count是整型变量)这样的简单操作也得加锁,因为即便是增量操作这样的操作,在汇编代码中实际上也是分三步进行的:读、改、写(回)。
movl x, %eax
addl $1, %eax
movl %eax, x
更进一步,甚至内存变量的赋值操作都不能保证是原子的,比如在32位环境下运行这样的函数
void setValue() 

     value = 0x100000006; 
}
所有的C/C++操作被认定为非原子的。执行的过程中,这两条指令之间也是可以被打断的,而不是一条原子操作(也就是所谓的写撕裂),所以修改共享数据的操作必须以原子操作的形式出现,这样才能保证没有其它线程能在中途插一脚来破坏相应数据。
而在使用锁机制的过程中,即便在锁的粒度(granularity)、负载(overhead)、竞争(contention)、死锁(deadlock)等需要重点控制的方面解决的很好,也无法彻底避免这种机制的如下一些缺点:
1、锁机制会引起线程的阻塞(block),对于没有能占用到锁的线程或者进程,将一直等待到锁的占有者释放锁资源后才能继续执行,而等待时间理论上是不可设置和预估的。

2、申请和释放锁的操作,增加了很多访问共享资源的消耗,尤其是在锁竞争(lock-contention)很严重的时候,比如这篇文章所说

Locks Aren't Slow; Lock Contention Is

3、现有实现的各种锁机制,都不能很好的避免编程开发者设计实现的程序出现死锁或者活锁的可能
4、优先级反转(prorithy inversion)和锁护送(Convoying)的现象
5、难以调试
无锁编程(Lock-Free)就是在某些应用场景和领域下解决以上基于锁机制的并发编程的一种方案。
无锁编程的概念做一般应用层开发的会较少接触到,因为多线程的时候对共享资源的操作一般是用锁来完成的。锁本身对这个任务完成的很好,但是存在性能的问题,也就是在对性能要求很高的,高并发的场景下,锁会带来性能瓶颈。所以在一些如数据库这样的应用或者linux 内核里经常会看到一些无锁的并发编程。

锁是一个高层次的接口,隐藏了很多并发编程时会出现的非常古怪的问题。当不用锁的时候,就要考虑这些问题。主要有两个方面的影响:编译器对指令的排序和cpu对指令的排序。它们排序的目的主要是优化和提高效率。排序的原则是在单核单线程下最终的效果不会发生改变。单核多线程的时候,编译器的乱序就会带来问题,多核的时候,又会涉及cpu对指令的乱序。memory-ordering-at-compile-time和memory-reordering-caught-in-the-act里提到了乱序导致的问题。

除了Windows版本,源码中还提供了AtomicPointer的另外两种实现。源文件位置:leveldb/port/atomic_pointer.h 
三.利用std::atomic实现AtomicPointer
std::atomic是C++11提供的原子模板类,std::atomic对int、char、 bool等数据类型进行原子性封装。原子类型对象的主要特点就是从不同线程访问不会导致数据竞争(data race)。从不同线程访问某个原子对象是良性 (well-defined) 行为,而通常对于非原子类型而言,并发访问某个对象(如果不做任何同步操作)会导致未定义 (undifined) 行为发生。因此使用std::atomic可实现数据同步的无锁设计。
#if defined(LEVELDB_CSTDATOMIC_PRESENT)
class AtomicPointer {
 private:
  std::atomic rep_;
 public:
  AtomicPointer() { }
  explicit AtomicPointer(void* v) : rep_(v) { }
  inline void* Acquire_Load() const {
    return rep_.load(std::memory_order_acquire);
  }
  inline void Release_Store(void* v) {
    rep_.store(v, std::memory_order_release);
  }
  inline void* NoBarrier_Load() const {
    return rep_.load(std::memory_order_relaxed);
  }
  inline void NoBarrier_Store(void* v) {
    rep_.store(v, std::memory_order_relaxed);
  }
};
#endif
四.利用内存屏障来实现AtomicPointer
// Define MemoryBarrier() if available
// Windows on x86
#if defined(OS_WIN) && defined(COMPILER_MSVC) && defined(ARCH_CPU_X86_FAMILY)
// windows.h already provides a MemoryBarrier(void) macro
// http://msdn.microsoft.com/en-us/library/ms684208(v=vs.85).aspx
#define LEVELDB_HAVE_MEMORY_BARRIER


// Gcc on x86
#elif defined(ARCH_CPU_X86_FAMILY) && defined(__GNUC__)
inline void MemoryBarrier() {
  // See http://gcc.gnu.org/ml/gcc/2003-04/msg01180.html for a discussion on
  // this idiom. Also see http://en.wikipedia.org/wiki/Memory_ordering.
  __asm__ __volatile__("" : : : "memory");
}
#define LEVELDB_HAVE_MEMORY_BARRIER


// Sun Studio
#elif defined(ARCH_CPU_X86_FAMILY) && defined(__SUNPRO_CC)
inline void MemoryBarrier() {
  // See http://gcc.gnu.org/ml/gcc/2003-04/msg01180.html for a discussion on
  // this idiom. Also see http://en.wikipedia.org/wiki/Memory_ordering.
  asm volatile("" : : : "memory");
}
#define LEVELDB_HAVE_MEMORY_BARRIER


// Mac OS
#elif defined(OS_MACOSX)
inline void MemoryBarrier() {
  OSMemoryBarrier();
}
#define LEVELDB_HAVE_MEMORY_BARRIER


// ARM
#elif defined(ARCH_CPU_ARM_FAMILY)
typedef void (*LinuxKernelMemoryBarrierFunc)(void);
LinuxKernelMemoryBarrierFunc pLinuxKernelMemoryBarrier __attribute__((weak)) =
    (LinuxKernelMemoryBarrierFunc) 0xffff0fa0;
inline void MemoryBarrier() {
  pLinuxKernelMemoryBarrier();
}
#define LEVELDB_HAVE_MEMORY_BARRIER
#endif


// AtomicPointer built using platform-specific MemoryBarrier()
#if defined(LEVELDB_HAVE_MEMORY_BARRIER)
class AtomicPointer {
 private:
  void* rep_;
 public:
  AtomicPointer() { }
  explicit AtomicPointer(void* p) : rep_(p) {}
  inline void* NoBarrier_Load() const { return rep_; }
  inline void NoBarrier_Store(void* v) { rep_ = v; }
  inline void* Acquire_Load() const {
    void* result = rep_;
    MemoryBarrier();
    return result;
  }
  inline void Release_Store(void* v) {
    MemoryBarrier();
    rep_ = v;
  }
};
#endif
从上面可以看出各个平台都有相应的MemoryBarrier()实现,比如说windows平台已经定义过MemoryBarrier(void)宏,可以直接使用。而linux平台的gcc则通过内联一条汇编指令__asm__ __volatile__("" : : : "memory");来自定义MemoryBarrier()。
MemoryBarrier()的作用是添加内存屏障,当这个MemoryBarrier()之前的代码修改了某个变量的内存值后,其他 CPU 和缓存 (Cache) 中的该变量的值将会失效,必须重新从内存中获取该变量的值。
内存屏障的基本用途是避免编译器优化指令 。有些编译器默认会在编译期间对代码进行优化,从而改变汇编代码的指令执行顺序,如果你是在单线程上运行可能会正常,但是在多线程环境很可能会发生问题(如果你的程序对指令的执行顺序有严格的要求)。而内存屏障就可以阻止编译器在编译期间优化我们的指令顺序,为你的程序在多线程环境下的正确运行提供了保障,但是不能阻止 CPU 在运行时重新排序指令。
举个例子,有下面的代码:
a = b = 0;
//thread1
a = 1
b = 2

//thread2
if (b == 2) {
   //这时a是1吗?
}
假设只有单核单线程1的时候,由于a和 b的赋值没有关系,因此编译器可能会先赋值b然后赋值a,注意单线程的情况下是没有问题的,但是如果还有线程2,那么就不能保证线程2看到b为2 的时候a就为1。再假设线程1改为如下的代码:
a = 1
complier_fence()
b = 2
其中complier_fence()为一条阻止编译器在fence前后乱序的指令,x86/64下可以是下面的汇编语句,也可以由其他语言提供的语句保证。asm volatile(“” ::: “memory”);此时我们能保证b的赋值一定发生在a赋值之后。那么此时线程2的逻辑是对的吗?还不能保证。因为线程2可能会先读取a的旧值,然后再读取b的值。从编译器来看a和b之间没有关联,因此这样的优化是可能发生的。所以线程2也需要加上编译器级的屏障。
if (b == 2) {
   complier_fence()
   //这时a是1吗?
}
加上了这些保证,编译器输出的指令能确保a,b之间的顺序性。注意a,b的赋值也可以换成更复杂的语句,屏障保证了屏障之前的读写一定发生在屏障之后的读写之前,但是屏障前后内部的原子性和顺序性是没有保证的。
当把这样的程序放到多核的环境上运行的时候,a,b赋值之间的顺序性又没有保证了。这是由于多核CPU在执行编译器排序好的指令的时候还是会乱序执行。这个问题在memory-barriers-are-like-source-control-operations里有很好的解释。这里不再多说。

同样的,为了解决这样的问题,语言上有一些语句提供屏障的效果,保证屏障前后指令执行的顺序性。而且,庆幸的是,一般能保证CPU内存屏障的语句也会自动保证编译器级的屏障。注意,不同的CPU的内存模型(即对内存中的指令的执行顺序如何进行的模型)是不一样的,很辛运的,x86/64是的内存模型是强内存模型,它对CUP的乱序执行的影响是最小的。

A strong hardware memory model is one in which every machine instruction comes implicitly withacquire and release semantics. As a result, when one CPU core performs a sequence of writes, every other CPU core sees those values change in the same order that they were written.

因此在x86/64上可以不用考虑CPU的内存屏障,只需要在必要的时候考虑编译器的乱序问题即可。

回到leveldb里的AtomicPointer,注意到其中几个成员函数都是inline,如果不是inline,其实没有必要加上内存屏障,因为函数能够提供很强的内存屏障保证。下面这段话摘自memory-ordering-at-compile-time:

In fact, the majority of function calls act as compiler barriers, whether they contain their own compiler barrier or not. This excludes inline functions, functions declared with thepure attribute, and cases where link-time code generation is used. Other than those cases, a call to an external function is even stronger than a compiler barrier, since the compiler has no idea what the function’s side effects will be. It must forget any assumptions it made about memory that is potentially visible to that function.

下面针对Acquire_Load和Release_Store假设一个场景:
//thread1
Object.var1 = a;
Object.var2 = b;
Object.var2 = c;
atomicpointer.Release_Store(p);

//thread2
user_pointer = atomicpointer.Acquire_Load();
get Object.var1
get Object.var2
get Object.var3

对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据。

注意acquire,release模型适合单生产者和单消费者的模型,如果有多个生产者,那么现有的保障是不足的,会涉及到原子性的问题。


参考链接:

Locks Aren't Slow; Lock Contention Is

memory-ordering-at-compile-time

memory-reordering-caught-in-the-act

An Introduction to Lock-Free Programming

Acquire and Release Fences

Memory Barriers Are Like Source Control Operations

Acquire and Release Semantics

并发编程系列之一:锁的意义

理解Memory Barrier(内存屏障)
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

May 20, 2024 ---- 据TrendForce集邦咨询研究,三大原厂开始提高先进制程的投片,继存储器合约价翻扬后,公司资金投入开始增加,产能提升将集中在今年下半年,预期1alpha nm(含)以上投片至年底将...

关键字: 晶圆 HBM 存储器

2024年5月18日,强国机械制造有限公司正式宣布,全力支持国家提出的“中国制造2050”战略。公司将把智能制造作为未来发展的核心方向,致力于在这一领域实现重大突破,提升中国制造业的全球竞争力。

关键字: 智能制造 物联网

每次与老友见面时总是免不了谈论以前美好的回忆,但时间渐长,我们的记忆也渐渐模糊,还有电子设备帮助我们留下痕迹,只是翻找起来有些许麻烦。不过我们倒是没有这样的困扰,那是因为我有铁威马NAS,无论什么时间的照片都能放在nas...

关键字: 数据中心 数据存储

合肥2024年5月18日 /美通社/ -- 5月17日,以"致新世界"为主题,国轩高科第13届科技大会在包河总部隆重启幕,瞄准用户最为关切的高安全性、长续航、快速充电等核心需求和痛点问题,重磅发布三大...

关键字: 国轩高科 快充 电芯 能量密度

上海2024年5月19日 /美通社/ -- 5月18日,一年一度佳通商用车胎产品日如期而至。结合新市场、新机遇、新挑战,佳通轮胎召开"数智赋能 佳境无限"为主题的2024年度商用车胎技术暨产品发布会,...

关键字: 轮胎 数字化 零部件 TPMS

NRT14 于 2025 年底竣工后,园区容量将提高到104 兆瓦, 以满足日本对下一代基础设施和无缝接入互联数据社区日益增长的需求 北京2024年5月20日 /美通社/ -- 世界领先的...

关键字: DIGITAL 人工智能 数字化 数据中心

北京2024年5月20日 /美通社/ -- 过去五年里,支付和收款方式日新月异,其发展和变化比过去五十年都要迅猛。从嵌入式数字商务的出现,到"一拍即付"的...

关键字: VI BSP PAY COM

杭州2024年5月20日 /美通社/ -- 5月20日,百世供应链旗下百世云仓在2024年全国网络大会上,宣布了其全面出海战略。聚焦于东南亚市场的新机遇,并积极推动品牌走向国际市场。 百世供应链召开2024年百世云仓全...

关键字: 供应链 网络 触点 软件

上海2024年5月20日 /美通社/ -- 仲夏伊始,光芒新生,5月17日,由上海工业商务展览有限公司主办的、以"拥抱新质生产力,助力新型工业化"为主题的第九届广东国际机器人及智能装备博览会(以下简称...

关键字: IAR 机器人 自动化 RS

开幕在即!SEMI-e第六届深圳国际半导体展将在深圳国际会展中心(宝安)4/6/8号馆拉开精彩帷幕!

关键字: 半导体
关闭
关闭