C++ 11 使用 atomic 原子操作实现无锁编程。修改原子类型的操作是原子操作,原子操作不会被线程调度机制打断,可以保证数据一致性。
1. 原子类型的使用
1.1 基本类型的原子类型
例 1:
atomic<int> num(0);
void add() {
for (int i = 0; i < 100000; i++) // 加10万次。
num++;
}
int main() {
thread t1(add);
thread t2(add);
t1.join();
t2.join();
cout << num << endl;
}
第 1 行使用原子类型的 num,第 11 行输出为 20 万。如果第 1 行使用 int 类型的 num,第 13 行输出会小于 20 万。
当临界区是基本类型时,使用原子类型特化后,当作基本类型使用,如例 1 的第 1 行。
当临界区是复合类型时,使用原子类型特化后,却不能当作普通复合类型使用,比如无法直接使用成员引用运算符引用成员。如例 2,无法通过 sum1.data 引用成员 data。
1.2 复合类型的原子类型
例 2:
struct Example {
Example(int x) :sum(x) {}
void add() {
for (int i = 0; i < 100000; i++) {
Item old = sum.load(memory_order_seq_cst);
while (!sum.compare_exchange_weak(old, old.data + 1));
}
}
struct Item {
Item(int x = 0) :data(x) {}
int data;
};
atomic<Item> sum;
};
int main() {
Example ex(0);
thread th1 = thread(&Example::add, &ex);
thread th2 = thread(&Example::add, &ex);
th1.join();
th2.join();
cout << ex.sum.load().data << endl;
}
函数 add 操作的是特化复合类型 Item 得到的 sum,关键部分是第 5、6 行,其中第 6 行是原子操作:
- 第 5 行获取临界区 sum 的当前值 old,其实就是临界区的一个拷贝。
- 第 6 行:如果 sum 等于 old,用 old.data + 1 更新 sum 并返回 true。否则用 sum 更新 old 并返回 false,重新开始原子操作。
函数compare_exchange_weak
desire = now; // 进入原子操作时期望临界区是这个值。
now.compare_exchange_weak(desire, update);
- 当前值 now 与期望值 desire 相等时,修改当前值 now 为设定值 update,返回 true。
- 当前值 now 与期望值 desire 不等时,将期望值 desire 修改为当前值 now,返回 false。
- 这个函数可能在满足 true 的情况下仍然返回 false,所以只能在循环里使用,否则可以使用它的 strong 版本。
原子操作理解
使用 while 循环:获取临界区对象的值并记录,开始原子操作。原子操作不会被线程调度机制打断。但进入原子操作需要时间,这段时间内临界区可能被其它线程修改,所以原子操作的第 1 步就是比较进入原子操作前后临界区对象的值是否被其它线程修改。如果被其它线程修改了,就更新记录值重新开始原子操作。如果没有被其它线程修改,就执行原子操作的第 2 步:修改它的值。
例 3 主要演示怎么处理具有复杂构造函数的临界区对象,这样的对象才具有一般性。
例 3:
struct Example {
Example(int x, int y) {
Item temp(x, y);
sum.store(temp, memory_order_seq_cst);
}
void add() {
for (int i = 0; i < 100000; i++) {
Item old = sum.load(memory_order_seq_cst);
Item nov = Item(old.data1 + 1, old.data2 + 1);
while (!sum.compare_exchange_weak(old, nov))
nov = Item(old.data1 + 1, old.data2 + 1);
// while (!sum.compare_exchange_weak(old, Item(old.data1 + 1, old.data2 + 1)));
}
}
struct Item {
Item() = default;
Item(int x, int y) :data1(x), data2(y) {}
int data1, data2;
};
atomic<Item> sum;
};
临界区是具有复杂构造函数的对象时:
- 使用第 3、4 行的方式初始化临界区对象。
- 主要操作有第8 至 11 行。注意不能缺少第 11 行,因为函数 compare_exchange_weak 会更新 old 的值,但不会更新 nov 的值。
- 还可以使用第8、12行,和第8 至 11 行等价。
2. 原子类型的探究
1. 原子操作的内存顺序
// 1. 松散
memory_order_relaxed
// 2. 获得 - 释放
memory_order_consume
memory_order_acquire
memory order_release
memory_order_acq_rel
// 3. 顺序一致
memory_order_seq_cst