4 - 原子操作库atomic
原子类型允许原子访问,这意味着不需要额外的同步机制就可执行并发的读写操作。没有原子操作,递增变量就不是线程安全的。因为编译器首先将值从内存加载到寄存器中,递增后再把结果保存回内存。另一个线程可能在递增操作执行中接触到内存,导致数据争用。
为使此情形线程安全且不显式地使用任何同步机制,原子类型std::atomic
便派上用场。
原子操作库
使用原子类型
首先,给出一段有数据争用的代码:
int counter = 0;
vector<thread> threads;
for (int i = 0; i < 10; ++i)
{
threads.push_back(thread([&counter] {
for (int i = 0; i < 100; ++i)
{
++counter;
this_thread::sleep_for(1ms);
}
}));
}
for (auto& t : threads)
{
t.join();
}
cout << "Result = " << counter << endl;
期望结果是1000,但结果总不是。接下来使用原子类型,解决数据争用问题:
atomic<int> counter = 0;
之后的结果就是1000了,但执行时间很明显长了,因此要尝试 最小化同步次数(最小化原子操作/显式同步操作次数)以优化代码:
threads.push_back(thread([&counter] {
int result = 0;
for (int i = 0; i < 100; ++i)
{
++result;
this_thread::sleep_for(1ms);
}
counter += result;
}));
一些概念
原子操作
C++标准定义了一些原子操作,这里简要介绍一两个操作。
首先是原子比较与交换操作:
bool atomic<T>::compare_exchange_strong(T& expected, T desired);
它以原子方式实现了以下逻辑:
if (*this == expected)
{
*this = desired;
return true;
}
else
{
expected = *this;
return false;
}
这是编写无锁并发数据结构的关键组件,它允许不使用任何同步机制来操作数据。
然后是fetch_add()
操作,它获取该原子类型的当前值,将给定的递增值添加到这个原子值,并返回未递增的原子值:
atomic<int> val = 10;
cout << "Value = " << val << endl;
int fetched = val.fetch_add(4);
cout << "Fetched = " << fetched << endl;
cout << "Value = " << val << endl;
// 输出结果
Value = 10
Fetched = 10
Value = 4
大部分原子操作可接受一个额外参数,用于指定想要的内存顺序,例如:
T atomic<T>::fetch_add(T value, memory_order = memory_order_seq_cst);
很少有必要使用默认之外的内存顺序,在这里查看其他内存顺序。
原子智能指针
早期C++标准中不允许创建atomic<shared_ptr<T>>
,因为shared_ptr
不可拷贝。shared_ptr
中存储引用计数的控制块一直是线程安全的,但其他内容(非 const 方法等)却不是。
C++20通过<memory>
引入了对atomic<shared_ptr<T>>
的支持,即使调用非 const 方法也是线程安全的。但 在其所指向的对象上调用非 const 方法仍不是线程安全的,需要手动同步。
原子引用
C++20也引入了atomic_ref
,和atomic
不同的地方就是它使用引用,而后者使用拷贝给它的值。可以创建多个atomic_ref
来引用同一个对象。
如果atomic_ref
实例引用某个对象,则 不允许在没有经过其中一个atomic_ref
实例的情况下接触该对象。
等待原子变量
C++20在std::atomic
和std::atomic_ref
中添加了如下方法,用于有效等待原子变量被修改:
方法 | 描述 |
---|---|
wait(oldValue) | 阻塞线程,直到另一个线程调用notify_one() 或notify_all() 且原子变量值已经改变。 |
notify_one() | 唤醒一个阻塞在wait() 调用上的线程。 |
notify_all() | 唤醒所有阻塞在wait() 调用上的线程。 |
以下是一个示例,它让子线程等待,直到value被改变:
atomic<int> value = 0;
jthread job([&value] {
cout << "Thread starts waiting..." << endl;
value.wait(0);
cout << "Thread wake up now, value = " << value << endl;
});
this_thread::sleep_for(2s);
cout << "Main thread change value to 1" << endl;
value = 1;
value.notify_all();
// 输出
Thread starts waiting...
Main thread change value to 1
Thread wake up now, value = 1
参考资料
- 飘零的落花 - 现代C++详解
- C++20高级编程