作者:陈聪捷
导语: C++ 11与JDK 1.9都新增了对memory order的支持,对于memory order这个概念,本文试图阐述清楚与它相关的问题的由来,概念定义以及c++ 11与jdk 1.9对其的支持。
在分析memory order之前,我们先讲一下为什么要考虑memory order问题,这里需要简单分析一下多线程编程环境中的内存模型。
上图所示的是一个典型的多核CPU系统架构,它包含有2个CPU核,每个CPU核有一个私有的32KB 的 L1 cache,两个CPU 核共享 1MB的 L2 cache 以及 512MB的主存。
在这个内存模型下,cpu写数据并不是立即写入RAM中,而是写入L1 cache,再从L1 cache存入(store) RAM中,读数据也是先从L1 cache中读,读不到再从RAM中读,这种读写数据的模式是能够提高数据存取效率的,但是在一些特殊情况下会导致程序出错,考虑以下这个例子。
x=y=0;
Thread 1 Thread 2
x = 1; y = 1;
r1 = y; r2 = x;
表面上看,r1==r2==0这种输出是不可能出现的,然而,有一种可能性是,由于r1不依赖于x,编译器可以把r1=y这步操作调整到x=1这步操作之前,同样,r2=x这步操作可以调整到y=1这步操作之前,这样一来,core 1可以先读取L1 cache中的y的值,core 2 才执行 y = 1的赋值操作,同理,r2 = x这步操作也可以在x=1这步赋值操作之前执行,这时候就会出现r1 == r2 ==0的输出结果。
如何避免这种情况的出现呢?最简单的方案是给x, y变量操作加互斥锁,然而,我们都知道,互斥锁会导致代码执行效率降低,那么,有没有其他同步原语,既能保证程序的正确性,又能尽可能地提高程序执行效率呢?下面介绍4种 Memory Barrier 。
理论上讲,Memory Barrier有4类,如下图所示。
下面分别进行分析。
LoadLoad 这种内存栅栏(memory barrier),顾名思义,就是阻止栅栏后面的load操作被调整到栅栏前面的load操作之前,类似于 git pull 或者 svn update 操作,如下图所示。
LoadLoad 的主要作用是防止程序加载已经过期的数据,考虑以下代码:
if (IsPublished) // Load and check shared flag
{
LOADLOAD_FENCE(); // Prevent reordering of loads
return Value; // Load published value
}
LOADLOAD_FENCE 在其中的作用是阻止读取Value这步操作被reorder到读取IsPublished这步操作之前,这样,只有在IsPublished置位后,才会去读取Value的值。
类似于LoadLoad,StoreStore 这种内存栅栏用于阻止栅栏后面的store操作被调整到栅栏前面的store操作之前,类似于git push或者svn commit操作,如下图所示。
同理,StoreStore可以避免将过期的数据写入内存。
Value = x; // Publish some data
STORESTORE_FENCE();
IsPublished = 1; // Set shared flag to indicate availability of data
LoadStore 内存栅栏用于保证所有在这个栅栏之前的load操作一定会在这个栅栏之后的store操作之前执行。例如:
IsPublished = X; // Load X and set IsPublished
LOADSTORE_FENCE();
Value = 1; // Publish some data
在这里,Value = 1 这步操作可以被提前到读取X的值这步操作之前,之所以允许这种优化,是因为有时候在L1 cache中没有缓存X的值,而已经缓存了Value=1这步操作,这时候先执行store再执行load效率会更高。然而,LoadStore这种栅栏可以阻止这种情况的发生。
StoreLoad 用于保证所有在这个栅栏之前的store操作一定会在这个栅栏之后的load操作之前执行,可以认为这是svn或者git中用户本地代码目录与central repository之间的一次同步操作,如下图所示。
StoreLoad 可以解决前文所说的r1==r2==0的问题,考虑将程序改成如下这种形式。
x=y=0;
Thread 1 Thread 2
x = 1; y = 1;
STORELOAD_FENCE(); STORELOAD_FENCE();
r1 = y; r2 = x;
在这种情况下,r1==r2==0这个情况是不会出现的。
Acquire与Release是无锁编程中最容易混淆的两个原语,它们是线程之间合作进行数据操作的关键步骤。在这里,借助前面对memory barrier的解释,对acquire与release的语义进行阐述。
用上面的memory barrier来描述,acquire等价于LoadLoad加上LoadStore栅栏。
用上面的memory barrier来描述,release等价于LoadStore加上StoreStore栅栏。
借助acquire与release语义,我们再重新来看一下互斥锁(mutex)如何用acquire与release来实现,实际上,mutex正是acquire与release这两个原语的由来,acquire的本意是acquire a lock,release的本意是release a lock,因此,互斥锁能保证被锁住的区域内得到的数据不会是过期的数据,而且所有写入操作在release之前一定会写入内存,如下图所示。
以上关于memory barrier的背景和相关概念说明的部分,有很多参考自Preshing on Programming博客,有兴趣的同学可以前往该博客阅读其博文,上面有不少实验也非常地有趣。
C++ 11 在标准中提出了6种同步操作: memory_order_relaxed, memory_order_acquire, memory_order_consume, memory_order_release, memory_order_acq_rel, memory_order_seq_cst,关于C++ 11的memory order,
这两篇文章有比较详细的描述,结合上述对一些专有名词的解释,这两篇文章应该比较容易看懂,这里不再赘述。
为了与C++ 11对齐,jdk 1.9标准中也新增了与memory order相关的同步操作,新添加了VarHandle这个类来封装相关的方法。
VarHandle类的设计目的是为了替代java.util.concurrent.atomic以及sun.misc.Unsafe这两个类中的一些方法,标准中指出这两个类的一些方法存在性能和可移植性问题,下面举例说明:
为了解决这些问题,JEP希望设计VarHandle这样一种变量类型,它能够支持在多种不同的访问模式下对变量进行读写操作,支持的变量类型包括对象域、静态域、数组元素以及一些不在堆上的用ByteBuffer描述的字节数组。
VarHandle类的访问模式包括以下几类:
后面三种内存访问模式通常被称为read-modify-write模式。
VarHandle类可以由MethodHandle类进行生产,代码示例如下:
class Foo {
int i;
...
}
...
class Bar {
static final VarHandle VH_FOO_FIELD_I;
static {
try {
VH_FOO_FIELD_I = MethodHandles.lookup().
in(Foo.class).
findVarHandle(Foo.class, "i", int.class);
} catch (Exception e) {
throw new Error(e);
}
}
}
在获取并返回一个VarHandle实例之前,MethodHandle的Lookup方法会进行访问控制权限检查。
如果要获取一个用于访问数组的VarHandle实例,可以采用以下方法。
VarHandle intArrayHandle = MethodHandles.arrayElementVarHandle(int[].class);
获取到VarHandle类实例后,如何用这个类去修改类的域呢?
Foo f = ...
boolean r = VH_FOO_FIELD_I.compareAndSet(f, 0, 1);
int o = (int) VH_FOO_FIELD_I.getAndSet(f, 2);
为了保证效率,VarHandle类的实例通常需要被声明为static final变量(其实就是常量),这样可以在编译期对它进行优化。
用VarHandle类反射获取MethodHandle类的方法如下:
Foo f = ...
MethodHandle mhToVhCompareAndSet = MethodHandles.publicLookup().findVirtual(
VarHandle.class,
"compareAndSet",
MethodType.methodType(boolean.class, Foo.class, int.class, int.class));
调用这个MethodHandle的代码如下:
boolean r = (boolean) mhToVhCompareAndSet.invokeExact(VH_FOO_FIELD_I, f, 0, 1);
MethodHandle mhToBoundVhCompareAndSet = mhToVhCompareAndSet
.bindTo(VH_FOO_FIELD_I);
boolean r = (boolean) mhToBoundVhCompareAndSet.invokeExact(f, 0, 1);
反射生成MethodHandle的另一种方案是:
MethodHandle mhToVhCompareAndSet = MethodHandles.varHandleExactInvoker(
VarHandle.AccessMode.COMPARE_AND_SET,
MethodType.methodType(boolean.class, Foo.class, int.class, int.class));
boolean r = (boolean) mhToVhCompareAndSet.invokeExact(VH_FOO_FIELD_I, f, 0, 1);
关于MethodHandle的更多使用方法可以参考文章理解JDK中的MethodHandle。
最后,陈列一下VarHandle类内部支持memory order的方法
/**
* Ensures that loads and stores before the fence will not be
* reordered with loads and stores after the fence.
*
* @apiNote Ignoring the many semantic differences from C and
* C++, this method has memory ordering effects compatible with
* atomic_thread_fence(memory_order_seq_cst)
*/
public static void fullFence() {}
/**
* Ensures that loads before the fence will not be reordered with
* loads and stores after the fence.
*
* @apiNote Ignoring the many semantic differences from C and
* C++, this method has memory ordering effects compatible with
* atomic_thread_fence(memory_order_acquire)
*/
public static void acquireFence() {}
/**
* Ensures that loads and stores before the fence will not be
* reordered with stores after the fence.
*
* @apiNote Ignoring the many semantic differences from C and
* C++, this method has memory ordering effects compatible with
* atomic_thread_fence(memory_order_release)
*/
public static void releaseFence() {}
/**
* Ensures that loads before the fence will not be reordered with
* loads after the fence.
*/
public static void loadLoadFence() {}
/**
* Ensures that stores before the fence will not be reordered with
* stores after the fence.
*/
public static void storeStoreFence() {}
通过上面的背景描述,我们可以知道,对于读操作,fullFence强于acquireFence强于loadLoadFence,对于写操作,fullFence强于releaseFence强于storeStoreFence。
JDK 1.9还提供了一种可达性屏障,定义在java.lang.ref.Reference类里面
class java.lang.ref.Reference {
// add:
/**
* Ensures that the object referenced by the given reference
* remains <em>strongly reachable</em> (as defined in the {@link
* java.lang.ref} package documentation), regardless of any prior
* actions of the program that might otherwise cause the object to
* become unreachable; thus, the referenced object is not
* reclaimable by garbage collection at least until after the
* invocation of this method. Invocation of this method does not
* itself initiate garbage collection or finalization.
*
* @param ref the reference. If null, this method has no effect.
*/
public static void reachabilityFence(Object ref) {}
}
program reorder这个问题其实在平时的开发中比较少会遇到,但是考虑到特定的cpu或者编译器在优化指令时会有重排序的情况,了解这些知识有助于调试一些疑难杂症。
参考文献
[1] Preshing on Programming博客 [2] jdk 1.9 标准
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。