我们在阅读Qt源码时,经常看到这样的代码:
d->threadData.loadRelaxed(); 。这在Qt中是一种常见的代码模式,其中 threadData 代表了对象所在线程的数据。loadRelaxed 是 QAtomicPointer 类的成员函数,它以松散(relaxed)的内存顺序加载原子指针的当前值。"松散"意味着加载操作不提供内存顺序保证,编译器和处理器可以自由重排序这个操作。
选择 loadRelaxed 而非更严格的内存顺序(如 loadAcquire)一般是因为性能考虑。在多线程程序中,更严格的内存顺序可能导致额外的同步需求,从而降低效率。开发者如果确定不需要额外同步,可能会选择 loadRelaxed 来优化性能。
那么,何时该用 loadAcquire,何时该用 loadRelaxed 呢?
使用 loadRelaxed 的场景:
**例子 1: 统计数据:**在一个多线程程序中,如果一个线程负责统计数据(如计数器),其他线程读取计数器值时不依赖于其特定值来保证逻辑正确性。此时,可以使用 loadRelaxed 读取值,因为不需要关心读取操作与其他内存操作的顺序。
std::atomic<int> counter{0};
void incrementCounter() {
counter.fetch_add(1, std::memory_order_relaxed); // 仅递增计数器,不需要同步
}
int getCounter() {
return counter.load(std::memory_order_relaxed); // 读取计数器的当前值,不需要同步
}
在这里,读取操作可能不立即看到最新更改,但对统计数据通常可接受。
使用 loadAcquire 的场景:
例子 2: 初始化共享资源:如果有一个共享资源,在程序启动时初始化一次,所有线程应确保在访问资源前看到初始化完成的状态。此时,初始化线程在完成后应使用 storeRelease 存储标志,其他线程使用 loadAcquire 读取标志,确保看到初始化的完整效果。
std::atomic<bool> initialized{false};
SomeResource* resource;
void initializeResource() {
resource = new SomeResource();// 初始化其他必要的状态...
initialized.store(true, std::memory_order_release); // 释放语义确保初始化完成
}
void useResource() {
while (!initialized.load(std::memory_order_acquire)) {
// 等待资源初始化
}// 现在安全地使用 resource,因为初始化已经完成
}
在这个例子中,loadAcquire 确保了所有在标志设置为 true 之后的读取操作都能看到 SomeResource 的初始化状态,防止了潜在的数据竞争。
简言之,使用 loadRelaxed 适用于单个原子操作同步,不关心与其他内存操作的顺序。当需要确保一系列操作的内存顺序以安全访问共享数据时,应使用 loadAcquire。正确选择内存顺序对于有效的并发控制至关重要。
正如我们之前强调的那样,避免在并发中使用锁,在迫不得已时可以使用std::atomic,而想要用好std::atomic又是另一个富有挑战性的故事了。在C++标准库的atomic会更加复杂,例如,std::atomic 支持以下内存顺序选项:
- memory_order_relaxed: 不提供任何同步或顺序保证。
- memory_order_consume: 仅在特定平台上提供同步。
- memory_order_acquire: 阻止在此操作之前的读写操作被重排序到此操作之后。
- memory_order_release: 阻止在此操作之后的读写操作被重排序到此操作之前。
- memory_order_acq_rel: 结合了 memory_order_acquire 和 memory_order_release的保证。
- memory_order_seq_cst: 提供顺序一致性保证,是最严格的内存顺序。
C++是面向系统级编程的编程语言,提供更丰富的原子操作的语义实现极致的性能。对于我们这样的应用开发者,尤其是非计算密集型的应用,通常只需理解相关代码和概念。
写出优秀的(比安全更进一步)多线程代码是一项具有挑战性的工作,其上限和下限都非常广阔。