对象与内存位置
1、内存模型
C++内存模型可以被看作是C++程序和计算机系统(包括编译器,多核CPU等可能对程序进行乱序优化的软硬件)之间的契约,它规定了多个线程访问同一个内存地址时的语义,以及某个线程对内存地址的更新何时能被其它线程看见。
2、对象与内存位置
在一个C++程序中的所有数据都是由对象(objects)构成。像int或float这样的对象就是简单基本类型;当然,也有用户定义类的实例。无论对象是怎么样的一个类型,一个对象都会存储在一个或多个内存位置上。需要注意的主要有4点:
1. 每一个变量都是一个对象,包括作为其成员变量的对象。
2. 每个对象至少占有一个内存位置
3. 基本类型都有确定的内存位置(无论类型大小如何,即使他们是相邻的,或是数组的一部分)。
4. 相邻位域是相同内存中的一部分。
原子类型
为了避免条件竞争,两个线程就需要一定的执行顺序。第一种方式,使用 互斥量来确定访问的顺序。另一 种方式是使用原子操作(atmic operations)同步机制。原子操作是一类不可分割的操作,它的状态要不就是完成,要不就是不做。
1、atomic_flag
std::atomic_flag 是最简单的标准原子类型,声明在<atomic>头文件。它表示了一个布尔标志。这个类型的对象可以 在两个状态间切换:设置和清除。std::atomic_flag 类型的对象必须被ATOMIC_FLAG_INIT初始化,初始化的状态为“”清除“”。
std::atomic_flag f = ATOMIC_FLAG_INIT;
atomic_flag对象使用clear()和test_and_set()成员函数来完成状态的切换。test_and_set检查状态是否被设置,若被设置直接返回true,若没有设置则设置为true后再返回false。clear是将状态置为false。下面是用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);
}
};
一开始,flag被初始化为false。当第一个线程调用lock使用互斥量时,test_and_set会将flag置为true,并返回false,程序跳出循环继续执行。这时,当第二个线程使用lock时,此时flag为true,test_and_set返回true,陷入while循环,直到有线程使用unlock释放互斥量。
原子类型的所有操作的都是原子的,他们的操作都是独立的。但是赋值和拷贝需要将2个独立的原子对象合并到一起操作,这违背了原子性。所以,原子类型既不支持拷贝构造,也不支持拷贝赋值。
2、atomic
C++提供了atomic<>模板类来生成指定类型的原子对象。atomic使用store()实现写入,exchange()使用新的值代替已存储的值,load()将原子类型转换为普通的值。
#include <iostream>
#include <atomic>
using namespace std;
int main(){
atomic<bool> b;
cout << b << endl; // 隐式转换
bool a = b.load(); // 显式转换
cout << a << endl;
b.store(true);
cout << b << endl;
b.exchange(false);
cout << b << endl;
return 0;
}
运行结果为:
0
0
1
0
std::atomic 和 std::atomic_flag 的不同之处在于, std::atomic 不是无锁的; 为了保证操作的原子性,其实现中需要一个内置的互斥量。
还有一种新型操作,叫做“比较/交换”,它的形式表现为compare_exchange_weak()和 compare_exchange_strong()成员函数。“比较/交换”操作是原子类型编程的基石;bool compare_exchange_strong(T& expected, T desired)能够自动比较*this与expected的值,如果二者相等,会将*this的值修改为desired的值(执行read-modify-write操作),否则将expected的值修改为*this的值。当*this被改变时compare_exchange_strong返回true,否则返回false。
3、atomic指针运算
c++使用atomic<T*>来声明原子指针类型。atomic<T*>和atomic<T>具有相同的成员函数与功能,但是它操作的十三类型的指针而非类型本身。同时,atomic<T*>还为指针运算提供了新的操作,例如fetch_add()和fetch_sub(),他们在存储地址上做原子的加法和减法,为+=,-=,++和--提供建议的封装。如,x是 std::atomic 类型的数组的首地址,然后x+=3让其偏移到第四个 元素的地址,并且返回一个普通的 Foo* 类型值,这个指针值是指向数组中第四个元素。fetch_add()和fetch_sub()的返回值略有不同(所以x.ftech_add(3)让x指向第四个元素,并且函 数返回指向第一个元素的地址)。
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]);
4、atomic<>类模板
为了使 用 std::atomic (UDT是用户定义类型),这个类型必须有拷贝赋值运算符。这就意味着这 个类型不能有任何虚函数或虚基类,以及必须使用编译器创建的拷贝赋值操作。不仅仅是这 些,自定义类型中所有的基类和非静态数据成员也都需要支持拷贝赋值操作。atomic<>支持用户自定义类型。但是并不是所有的自定义类型都可以。
最后,这个类型必须是“位可比的”(bitwise equality comparable)。你不仅需要确定,一个UDT类型对象可以使用memcpy()进行拷贝,还要确定其对象可以使用 memcmp()对位进行比较。
当使用用户定义类型T进行实例化时, std::atomic 的可用接口就只有: load(), store(), exchange(), compare_exchange_weak(), compare_exchange_strong()和赋值操作,以及向类型T转换的操作。