关闭

Intel Threading Building Blocks 编程指南:原子操作

303人阅读 评论(0) 收藏 举报
分类:

原子操作概述

    可以使用原子操作来避免使用互斥。当一个线程执行原子操作,在其他线程眼里,这个操作是瞬时完成的。原子操作的优点是,相比较锁操作是快速的,而且不用为死锁、锁护送等问题而烦恼。缺点是,它们只有有限的一组操作,常常无法和成为有效的复杂操作。尽管如此,也不应该放弃使用原子操作替换互斥的机会。aotmic<T> 类以C++风格实现了原子操作。   

     原子操作的一个典型应用是线程安全的引用计数。设x是类型为 int 的引用计数,当它变为0时程序需要做一些操作。在单线程代码中,你可以使用 int 来定义 x,然后 --x;if ( x==0 ) action() 。但在多线程环境中,这种方法可能会失效,因为两个线程可能以下表的方式交替操作(其中的t(x)代表机器的寄存器)。


虽然代码想要把 x 递减两次,但是结果只是比初始值少了1。还有另外一个问题结果,因为对x值的检查与做递减操作是分开进行的,如果 x 的初始值是 2,而且两个线程都在执行 if 条件语句前将 x 的值减 1 ,那么两个线程都会调用 action() 。要修正这个问题,你需要确保同一时间只有一个线程做递减,而且确保“if”检查的值的确是递减后的。使用互斥体可以做到这点。更为简单且快速的方法是将 x 声明为 atomic<int> ,并且这么写:
<span style="font-size:18px;"><span style="font-size:14px;">if(--x==0) action()</span></span>
atomic<T>重载了 "--" 操作符;这步操作不会有别的线程介入。atomic<T> 模板支持的类型 T 包括:整形、枚举或者指针。支持5种基本操作,并且为了语法上的方便以重载操作符的方式提供了额外的接口。例如,atomic<T>中的  ++, --, -=, 和 += 操作其实都是基本操作 fetch-and-add 的变形。下表列出了原子操作模板的5种基本操作:
= 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>
    例程 GetUniqueInteger 每被调用一次就返回一个不同的整形,直到计数器又从头计数。无论多少个线程同时执行这段代码,都不会出例外。
    compare_and_swap 是很多非阻塞算法的基本操作。互斥体的一个问题是,如果持有某个锁的线程挂起了,其他所有线程在它恢复之前都会被阻塞。非阻塞算法用原子操作代替锁来避免这个问题。他们(非阻塞算法)通常很复杂,而且需要复杂的分析去验证。然而,下面的习惯很直观,值得知晓。它以一种基于 globalx 旧值的方式更新 globalx 。
<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>
比较差的情况下,一些线程迭代循环直到没有其他的线程干预。一般来说,如果更新只需要少数指令,这种方法要快于相应的互斥体解决方案。
注意:如果下述序列不利于你的意图,那么上述的更新方法就不可取:
  1. 一个线程从 globalx 中读取值 A
  2. 其他的线程将 globalx 从 A 修改为 B ,再到 A
  3. 步骤1 的线程执行 compare_and_swap, 读取 A ,但没有检测到期间变化到 B   
这个问题被称为 ABA 问题。为链表数据结构设计设计非阻塞算法时,它常常成为问题。(google下更多的信息)

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 才会进行数据结构的读操作。
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:254624次
    • 积分:3398
    • 等级:
    • 排名:第10325名
    • 原创:5篇
    • 转载:500篇
    • 译文:0篇
    • 评论:8条
    最新评论