LevelDB源码分析之四:AtomicPointer

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-timememory-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<void*> 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(内存屏障)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

草上爬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值