一.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)等等。之所以会出现无锁编程技术,因为基于锁的编程的有如下缺点。movl x, %eax
addl $1, %eax
movl %eax, x
更进一步,甚至内存变量的赋值操作都不能保证是原子的,比如在32位环境下运行这样的函数
2、申请和释放锁的操作,增加了很多访问共享资源的消耗,尤其是在锁竞争(lock-contention)很严重的时候,比如这篇文章所说
Locks Aren't Slow; Lock Contention Is
3、现有实现的各种锁机制,都不能很好的避免编程开发者设计实现的程序出现死锁或者活锁的可能锁是一个高层次的接口,隐藏了很多并发编程时会出现的非常古怪的问题。当不用锁的时候,就要考虑这些问题。主要有两个方面的影响:编译器对指令的排序和cpu对指令的排序。它们排序的目的主要是优化和提高效率。排序的原则是在单核单线程下最终的效果不会发生改变。单核多线程的时候,编译器的乱序就会带来问题,多核的时候,又会涉及cpu对指令的乱序。memory-ordering-at-compile-time和memory-reordering-caught-in-the-act里提到了乱序导致的问题。
除了Windows版本,源码中还提供了AtomicPointer的另外两种实现。源文件位置:leveldb/port/atomic_pointer.h#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()。
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的赋值也可以换成更复杂的语句,屏障保证了屏障之前的读写一定发生在屏障之后的读写之前,但是屏障前后内部的原子性和顺序性是没有保证的。
同样的,为了解决这样的问题,语言上有一些语句提供屏障的效果,保证屏障前后指令执行的顺序性。而且,庆幸的是,一般能保证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.
回到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.
//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,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
注意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
并发编程系列之一:锁的意义