原子操作概述
可以使用原子操作来避免使用互斥。当一个线程执行原子操作,在其他线程眼里,这个操作是瞬时完成的。原子操作的优点是,相比较锁操作是快速的,而且不用为死锁、锁护送等问题而烦恼。缺点是,它们只有有限的一组操作,常常无法和成为有效的复杂操作。尽管如此,也不应该放弃使用原子操作替换互斥的机会。aotmic<T> 类以C++风格实现了原子操作。
原子操作的一个典型应用是线程安全的引用计数。设x是类型为 int 的引用计数,当它变为0时程序需要做一些操作。在单线程代码中,你可以使用 int 来定义 x,然后 --x;if ( x==0 ) action() 。但在多线程环境中,这种方法可能会失效,因为两个线程可能以下表的方式交替操作(其中的t(x)代表机器的寄存器)。
<span style="font-size:18px;"><span style="font-size:14px;">if(--x==0) action()</span></span>
= x | 读取 x 的值 |
x = | 给 x 赋值,并返回它 |
x.fetch_and_store(y) | 执行x=y,并返回x的旧值 |
x.fetch_and_add(y) | 执行x+=y,并返回x的旧值 |
x.compare_and_swap(y,z) | 如果x==z,执行 x=y . 返回x的旧值 |
<span style="font-size:18px;">atomic<unsigned> counter;
unsigned GetUniqueInteger()
{
return counter.fetch_and_add(1);
}</span>
<span style="font-size:18px;">atomic<int> globalx;
int UpdateX()
{ // Update x and return old value of x.
do
{
// Read globalX
oldx = globalx;
// Compute new value
newx = ...expression involving oldx....
// Store new value if another thread has not changed globalX.
} while (globalx.compare_and_swap(newx, oldx) != oldx);
return oldx;
}</span>
-
一个线程从 globalx 中读取值 A
-
其他的线程将 globalx 从 A 修改为 B ,再到 A
-
步骤1 的线程执行 compare_and_swap, 读取 A ,但没有检测到期间变化到 B
atomic<T>没有构造函数
atomic<T>模板类特意没有声明构造函数,因为诸如上述的 GetUniqueInteger 之类的例子一般要求在所有的文件作用域构造函数被调用前就可以工作。如果该模板类声明了构造函数,在它被引用后,也许要初始化一个文件作用域的实例。在下述上下文中,任何没有生命构造函数的 C++类的原子类型atomic<T> 的对象 X 被自动初始化为 0 :
-
X 被声明为文件作用域变量,或者类的静态数据成员
-
X 是类的成员,并且显式地出现在该类的构造函数的初始化列表中
下面的代码是对这些问题的解释
<span style="font-size:18px;"><span style="font-size:14px;">atomic<int> x; // 由于处于文件作用域,初始化为0
class Foo
{
atomic<int> y;
atomic<int> notzeroed;
static atomic<int> z;
public:
Foo() :
y() // y 初始化为0.
{
// notzeroed has unspecified value here.
}
};
atomic<int> Foo::z; // 静态成员,初始化为0</span></span>
内存一致性
一些计算机架构,比如Intel IA-64(安腾)系列,拥有“弱内存一致性”,对不同地址的内存操作出于效率方面的原因被重新排序。这是个复杂的话题,建议感兴趣的读者查阅其他资料。如果只是为IA-32 和 Intel 64 架构平台编程,可以忽略此节。
atomic<T> 类准许你强制某些内存排序操作,在下表列出:
种类 | 描述 | 默认为 |
获取(acquire) | 原子操作之后的操作不会挪动它 | 读 |
释放 (release) | 原子操作之前的操作不会挪动它 | 写 |
连续性一致 | 任何一边的操作都不会挪动原子操作。并且连续性一直的原子操作有着总的顺序。 | fetch_and_store fetch_and_add compare_and_swap |
最右边列出了特定约束的默认操作。使用这些默认值来避免不期望的意外。对于读和写,默认值是仅有的有效约束。然而,如果你很熟悉弱内存一致性,你也许会想改变其他操作默认的连续一致性为弱约束。要做到这点,使用接受模版参数的变量。参数可以是 acquire 或者 release (枚举类型 memory_semantics 的值)。
例,假设一份数据结构的不同部分由不同的线程生成,完成后,你想通知一个订阅线程。一种方法是初始化一个原子计数为生产者的数量,当每个生产者结束时,执行:
<span style="font-size:18px;">refcount.fetch_and_add<release>(-1);</span>
参数 release 确保在 refcount 做减法之前生产者写共享内存。类似,如果订阅者检查 refcount ,它必须使用 acquire(默认为读)栅格,这样订阅者直到看见 refcount 变为 0 才会进行数据结构的读操作。