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::atomicstd::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高级编程