AtomicPointer解读-leveldb源码剖析(2)

AtomicPointer的实现是平台(体系结构)相关,封装了对单个指针变量的读写操作,并分别提供了有无内存屏障的版本,本文仅讨论在Linux x86_64下的实现。

// 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

// 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;
  }
};

MemoryBarrier函数实现是inline且嵌入一条汇编指令__asm__ __volatile__(“” : : : “memory”);,__volatile__表示阻止编译器对该值进行优化,强制变量使用精确内存地址(非 cache或register),memory表示对内存有修改操作,需要重新读入,该指令能够阻止编译器乱序,但不能阻止CPU乱序执行(在SMP体系下),因此从操作上看,个人认为leveldb的AtomicPointer在多核体系下进行多线程读写操作并非线程安全(thread safety),但对于多读单写却是安全的,这点在SkipList的实现中有体现。

首先需要明确AtomicPointer 封装了对单个指针变量的读写(Load/Store)操作,在多线程下对指针变量的赋值或读操作是原子性的,不需要额外加锁,这点在Intel 64和X86_32体系开发文档中有明确说明如下:

8.1.1 Guaranteed Atomic Operations
The Intel486 processor (and newer processors since) guarantees that the following basic memory operations will always be carried out atomically:
• Reading or writing a byte
• Reading or writing a word aligned on a 16-bit boundary
• Reading or writing a doubleword aligned on a 32-bit boundary
The Pentium processor (and newer processors since) guarantees that the following additional memory operations will always be carried out atomically:
• Reading or writing a quadword aligned on a 64-bit boundary
• 16-bit accesses to uncached memory locations that fit within a 32-bit data bus
The P6 family processors (and newer processors since) guarantee that the following additional memory operation will always be carried out atomically:
• Unaligned 16-, 32-, and 64-bit accesses to cached memory that fit within a cache line

但是内联代码的嵌入并没有上下文代码块的顺序性保证,多线程读写共享数据会有问题。

再看Acquire_Load和Release_Store的具体实现,两个函数均为inline内联,如果是非inline函数就不需要考虑编译时乱序了,因为大多数非inline函数具有天然的内存屏障:

inline void* Acquire_Load() const {
    void* result = rep_;
    MemoryBarrier();
    return result;
  }

Acquire_Load使用临时变量result保存持有的指针变量,然后插入内存屏障再返回该值,防止编译器的合并优化,也避免多线程下其它线程修改该指针变量影响本线程后续使用。
这里有个强保证,在单个CPU下观察,Acquire_Load调用之后的代码获取的指针值是一致的。

inline void Release_Store(void* v) {
    MemoryBarrier();
    rep_ = v;
  }

Release_Store插入内存屏障防止编译器对修改指针变量的写操作与上文其它操作进行乱序重排。
这里也强保证,在单个CPU下观察,Release_Store调用之前的代码一定发生在修改该指针变量之前。

实际Acquire和Release搭配使用能形成一个完整的强Memory Barrier,当然还是得在单CPU下观察。

这里需要强调leveldb这个AtomicPointer并非严格按照Acquire和Release的语义去实现的,因为仅凭MemoryBarrier()函数是无法阻止CPU乱序的,多CPU下依赖现有实现是不靠谱的。

refer:
1. Intel® 64 and IA-32 Architectures Software Developer’s Manual
2. http://www.parallellabs.com/2010/04/15/atomic-operation-in-multithreaded-application/
3. https://www.zhihu.com/question/27026846/answer/92701793
4. http://hedengcheng.com/?p=803
5. http://preshing.com/20120625/memory-ordering-at-compile-time/

AtomicPointer解读-leveldb源码剖析(2)》上有5条评论

  1. jonny

    我有个不是很清楚的地方:leveldb的memory_barrier()应该是可以防止CPU乱序而不仅仅是防止编译器乱序的啊?博主说“这里需要强调leveldb这个AtomicPointer并非严格按照Acquire和Release的语义去实现的,因为仅凭MemoryBarrier()函数是无法阻止CPU乱序的,多CPU下依赖现有实现是不靠谱的。” 请问能否具体说明?leveldb在多核环境下实际应用应该没问题才对

    回复
    1. pandademo 文章作者

      1. leveldb MemoryBarrier()这里只是一个解决编译时乱序的内存屏障,而不是SMP下解决运行时CPU乱序的内存屏障例如smp_mb(), smp_wmb(), smp_rmb()等,需要区分下。
      2. leveldb在多核下没有任何问题,在于其读查memtable和sstable,写memtable,memtable虽没有锁但从应用上是没有问题的,可以从skip_list实现来看相关指针操作是安全的

      回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注