第5章 C++内存模型和原子类型操作(C++并发编程实战)

5.2 C++中的原子操作和原子类型

原子操作时不可分割的操作。在系统的所有线程中,你是不可能观察到原子操作完成了一半这种情况;它要么做了,要么就没有做,只有这两种可能。如果从对象读取值的加载操作时原子的,那么对这个对象的所有修改操作也是原子的;要么加载操作得到的值时对象的初始值,要么是某次修改操作存入的值。

5.2.1 标准原子类型

标准原子类型定义在头文件<atomic>中。这些类型上的所有操作都是原子的,在语言定义中只有这些类型的操作时原子的,不过你可以用互斥锁来模拟原子操作。(模拟的方法:它们几乎都有一个is_lock_free()成员函数,如果是直接原子操作返回true,如果编译器和库内部用了一个锁的返回false)。

只用std::atomic_flag类型不提供is_lock_free()成员函数。这个类型是简答的布尔标志,并且在这种类型上的操作都是无锁的,它只提供了test_and_set()成员函数和clear()成员函数。

剩下的原子类型都可以通过特化的std::atomic<>类型模板而访问到,并且拥有更多的功能,但可能不都是无锁的。

除了直接使用atomic<>类型模板外,你也可以使用下列表中的原子类型集。

表5.1如下是标准原子类型的备选名和与其相关的std::atomic<>特化类。

 表5.2标准原子类型定义(typedefs)和对应的内置类型定义(typedefs)

 

通常,标准原子类型是不能拷贝和赋值的,它们没有拷贝构造函数和拷贝赋值操作。但是它们可以隐式转换成对应的内置类型,所以这些类型依旧支持赋值,可以使用load()和store()成员函数,exchange、compare_exchange_weak()和compare_exchange_strong()。它们支持复合赋值:+=,-=,*=,|=等等。并且使用整型和指针的特化类型还支持++和--。当然,这些操作也有功能相同的成员函数相对应:fetch_add(),fetch_or等等。但是赋值操作和成员函数的返回值要么是被存储的值(赋值操作),要么是操作前的值(命名函数)。这能避免赋值操作返回引用。为了获取存储的引用的值,代码需要执行单独的读操作, 从而允许另外一个线程在赋值和读取操作的同时修改这个值,这也为条件竞争打开了大门。

std::atomic<>它是个通用的模板,操作被限定为load(),store()(赋值和转换为用户类型),exchange(),compare_exchange_weak()和compare_exchange_strong()。

每个函数的类型的操作都有一个可选的内存排序参数,这个参数用来指定所需存储的顺序。如下是分类

1.store操作,可选顺序:memory_order_relaxed,memory_order_release,memory_order_seq_cst。

2.load操作,可选顺序:memory_order_relaxed,memory_order_consume,memory_order_acquire,memory_order_seq_cst。

3.read-modify-write(读-改-写)操作,可选顺序:memory_order_relaxed,memory_order_consume,memory_order_acquire,memory_order_release,memory_oreder_acq_rel,memory_order_seq_cst。

所有操作的默认顺序为都是memory_order_seq_cst。

5.2.2 std::atomic_flag的相关操作

std::atomic_flag是最简单的标准原子类型,它表示了一个布尔标志。这个类型可以在两个状态间切换:设置和清除。

std::atomic_flag类型的对象必须被ATOMIC_FLAG_INIT初始化。初始化标准为是清除状态:

std::atomic_flag f = ATOMIC_FLAG_INIT;

当你的标志对象已初始化,那么你只能做三件事情:销毁,清除或设置(查询之前的值)。这些事情对应的函数分别是:clear()成员函数,和test_and_set()成员函数。clear()和test_and_set()成员函数可以指定内存顺序。clear()是一个存储操作,所以不能有memory_order_acquire和memory_order_acq_rel语意,但是test_and_set是一个"读-改-写"操作,所有可以应用任何的内存顺序标签,默认的内存顺序是memory_order_seq_cst。例如:

f.clear(std::memory_order_release);	//1
bool x = f.test_and_set();	//2

清单5.1使用std::atomic_flag实现自旋互斥锁:

class spinlock_mutex
{
	std::atomic_flag flag;
public:
	spinlock_mutex():flag(ATOMIC_FLAG_INIT){}
	
	void lock()
	{
		while(flag.test_and_set(std::memory_order_acquire));
	}
	
	void unlock()
	{
		flag.clear(std::memory_order_release);
	}
};

5.2.3 std::atomic的相关操作

最基本的原子布尔类型是std::atomic<bool>,它有着比std::atomic_flag更加齐全的布尔特性。虽然它依然不能拷贝构造和赋值,但是你可以用一个非原子的bool类型构造它,并且你也可以从一个非原子bool变量赋值给std::atomic<bool>的实例:

std::atomic<bool> b(true);
b = false;

虽然内存顺序语义指定,但是使用store()去写入还是好于std::atomic_flag中限制性的很强的clear()。同样,test_and_set()函数可以被更加通用的exchange()成员函数所替代。exchange()允许你用新的值替代旧的值,并且自动的检索原来的值。std::atomic<bool>也支持对值的普通(不可修改)查找,其会将对象隐式转换为一个普通bool值,或显示调用load()来完成。store()是存储操作,load是加载操作,exchange是一个读-改-写操作:

std::atomic<bool> b;
boo x = b.load(std::memory_order_acquire); //x为b中存储的普通bool值
b.store(true);
x = b.exchange(false,std::memory_order_acq_rel);

exchange()的表现形式为:compare_exchange_weak和compare_exchange_strong()成员函数。它比较原子变量的当前值和一个期望值,当两值相等,存储提供值;当两值不相等,期望值就会被更新为原子变量中的值。返回true表示两值相等,false更新期望值(第一个参数为期望值,第二个参数为原始值)。

compare_exchange_weak可以伪失败:当原始值于预期值一致,存储也可能不会成功,并且compare_exchange_weak返回的是false。

因为compare_exchange_weak可以伪失败,所以这里通常使用于循环:

bool expected = false;
extern atomic<bool> b; //设置了什么
while(!b.compare_exchange_weak(expected,true) && !expected);

在这个例子中expected的值时钟为false,表示compare_exchange_weak会莫名失败。

std::atomic<bool>和std::atomic_flag的不同之处在于:std::atomic<bool>不是无锁的;为了保证操作的原子性,其实现中需要一个内置的互斥量。当处于特殊情况下时,你可以使用is_lock_free()成员函数,来检查std::atomic<bool>上的操作是否无锁。这是另一个,除了std::atomic_flag之外,所有原子类型都拥有的特征。

5.2.4 std::atomic:指针操作

原子指针类型,可以使用内置类型或自定义类型T,通过特化的std::atomic<T*>进行定义,就如同使用std::atomic<bool>类型一样。虽然几乎接口一致,但是它的操作是对于相关类型的指针,而非bool值本身。和std::atomic<bool>一样,既不能拷贝构造,也不能拷贝赋值。但是它可以通过合适的类型指针进行构造和赋值。std::atomic<T*>也包含is_lock_free(),load(),store(),exchange(),compare_exchange_weak和compare_exchange_strong()成员函数,与std::atomic<bool>的语意相同,获取与返回的类型都是T*,而不是bool。

std::atomic<T*>为指针提供新的操作。基本操作有fetch_add和fetch_sub提供,它们地址上原子加法和减法,为+=,-=,++和--提供简易封装。对于内置类型的操作:如果x是std::atomic<Foo*>类型的数组地址,然后x+=3让其便宜到第四个元素的地址,并且返回一个普通的Foo*类型值,这个指针指向数组的第四个元素。和fetch_add和fetch_sub返回值不同的是(x.fetch_add(3))让x指向第四个元素,函数返回的是指向第一个元素的地址。这种操作也被称为“交换-相加”。对于原子的读-改-写操作:exchange和compare_exchange_weak/compare_exchange_strong()一样,返回值是T*,而非std::atomic<T*>的引用:

class Foo{};
Foo some_array[5];
std::atomic<Foo*> p(some_array);
Foo* x = p.fetch_add(2); //p+2,并返回原始值
assert(x == some_array);
assert(p.load() == &some_array[2]);
x = (p -= 1);	//p-1,返回原始值
assert(x == &some_array[1]);
assert(p.load() == &some_array[1]);

//内存顺序语句
p.fetch_add(3,std::memory_order_release);

5.2.5 标准的原子整型相关的操作

如同普通的操作集合一样(load,store,exchange,compare_exchange_weak和compare_exchange_strong),在std::atomic<int>和std::atomic<unsigned long long>也有一套完整的操作:fetch_add,fetch_sub,fetch_and,fetch_or和fetch_xor,还有复合方式(+=,-=,&=,|=和^=),以及++和--(++x,x++,--x和x--)。除了除法、乘法和移位操作不造其中,如果需要,额外的操作可以将compare_exchange_weak放入循环中完成。

5.2.6 std::atomic<>主要类的模板

自定义的类型来创建原子变量的要求:为了使用std::atomic<UDT>(UDT为用户自定义类型),这个类型必须有拷贝赋值运算符。这意味着这个类型不能有任何虚函数或虚基类,以及必须使用编译器创建的拷贝赋值操作。自定义类型中所有的基类和非静态数据成员也都需要支持拷贝赋值操作。这就允许编译器使用memcpy,或赋值操作的等价操作,因为它们实现中没有用户代码。最后,这个类型必须是“位可比的”(bitwise equality comparable)——确定对象可以可以使用memcmp对位进行比较,是为了保证“比较/交换”操作能正常工作。

如下是使用用户定义类型T进行实例化时,std::atomic<T>可用的接口和赋值操作,以及向类型T的转换:

5.2.7 原子操作的释放函数 

对于大多数原子非成员函数(即C风格的原子操作)与对应的成员函数有关,但是需要"atomic_"作为前缀(比如atomic_load())。这些函数都会被不同的原子类型所重载。对于指定内存序列的标签时,_explicit作为后缀的时候需要额外的一个参数,否则就是默认的内存序。

释放函数的设计为了与C语言兼容,在C中只能使用指针,而不能使用引用。

C++标准库也有对一个原子类型中的std::shared_ptr<>智能指针提供释放函数。可使用的原子操作(load,store,exchange,compare_exchange_weak/compare_exchange_strong)都重载了标准原子操作,获取一个std::shared_ptr<>*作为第一个参数:

std::shared_ptr<my_data> p;
void process_global_data()
{
	std::shared_ptr<my_data> local = std::atomic_load(&p);
	process_data(local);
}

void update_global_data()
{
	std::shared_ptr<my_data> local(new my_data);
	std::atomic_store(&p,local);
}

作为和原子操作一同使用的其他类型,也提供_explicit变量,允许你指定所需的内存顺序,并且std::atomic_is_lock_free()函数可以用来确定实现是否使用锁,来保证原子性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值