C++并发:原子操作、内存模型、内存屏障
发布网友
发布时间:2024-10-05 07:34
我来回答
共1个回答
热心网友
时间:2024-11-16 18:10
原子操作是指在执行过程中不会被中断的操作,要么全部执行成功,要么全部不执行,不会出现部分执行的情况。原子操作可以看作是不可分割的单元,运行期间不会有任何的上下文切换。原子操作类常用成员函数有fetch_*、store、load、exchange、compare_exchange_weak和compare_exchange_strong:
fetch_*:先获取值再计算,即返回的是修改之前的值。
store:写入数据。
load:加载并返回数据。
exchange:直接设置一个新值。
compare_exchange_weak:先比较第一个参数的值和要修改的内存值(第二个参数)是否相等,如果相等才会修改,该函数有可能在except == value时也会返回false所以一般用在while中,直到为true才退出。
compare_exchange_strong:功能和*_weak一样,不过except == value时该函数保证不会返回false,但该函数性能不如*_weak。
注意:使用操作符(如+=、++、^=等)时要看类成员是否提供对应操作符,否则可能出现意想不到的问题。
C++标准库中的原子类型,如atomic,用于实现原子操作。它提供了一种线程安全的方式来对特定类型的数据进行读取和写入,以及执行其他常见的原子操作,如增加(增量)和交换等。
内存模型描述的是程序在执行过程中内存操作正确性的问题。内存操作包括读操作和写操作,每一操作又可以用两个时间点界定:发出(Invoke)和响应(Response)。内存一致性模型描述的就是这些操作可能的执行顺序中哪些是正确的。
内存序问题:内存序(memory order)问题是由于多线程的并行执行可能导致的对共享变量的读写操作无法按照程序员预期的顺序进行。多线程运行时,可能出现指令执行级别的优化:乱序优化,流水线,乱序执行,分支预测等合理重排。因此需要内存序来*CPU对指令执行顺序的重排程度。
happens-before和synchronizes-with语义
如果两个操作之间存在依赖关系,并且一个操作一定比另一个操作先发生,那么者两个操作就存在happens-before关系;synchronizes-with关系指原子类型之间的操作,如果原子操作A在像变量X写入一个之后,接着在同一线程或其它线程原子操作B又读取该值或重新写入一个值那么A和B之间就存在synchronizes-with关系;注意这两中语义只是一种关系,并不是一种同步约束,也就是需要我们编程去保证,而不是它本身就存在
memory_order模式
C++11中引入了六种内存约束符用以解决多线程下的内存一致性问题(在头文件中),其定义如下:
Sequential consistency模型又称为顺序一致性模型,是控制粒度最严格的内存模型。在顺序一致性模型下,程序的执行顺序与代码顺序严格一致,也就是说,在顺序一致性模型中,不存在指令乱序。
Relax模型对应的是memory_order中的memory_order_relaxed。从其字面意思就能看出,其对于内存的*最小,也就是说这种方式只能「保证当前的数据访问是原子操作(不会被其他线程的操作打断)」,但是对内存访问顺序没有任何约束,也就是说对不同的数据的读写可能会被重新排序。
Acquire-Release模型的控制力度介于Relax模型和Sequential consistency模型之间。其定义如下:
假设有一个原子变量A,对其进行写操作X的时候施加了memory_order_release约束符,则在当前线程T1中,该操作X之前的任何读写操作指令都不能放在操作X之后。当另外一个线程T2对原子变量A进行读操作的时候,施加了memory_order_acquire约束符,则当前线程T1中写操作之前的任何读写操作都对线程T2可见;当另外一个线程T2对原子变量A进行读操作的时候,如果施加了memory_order_consume约束符,则当前线程T1中所有原子变量A所依赖的读写操作都对T2线程可见(没有依赖关系的内存操作就不能保证顺序)。
一个对原子变量的「load操作」时,使用memory_order_acquire约束符:在「当前线程」中,该load之后读和写操作都不能被重排到当前指令前。如果「其他线程」使用memory_order_release约束符,则对此原子变量进行store操作,在当前线程中是可见的。
假设有一个原子变量A,如果A的读操作X施加了memory_order_acquire标记,则在当前线程T1中,在操作X之后的所有读写指令都不能重排到操作X之前;当其它线程如果对A进行施加了memory_order_release约束符的写操作Y,则这个写操作Y之前所有的读写指令对当前线程T1是可见的。
一个「load操作」使用了memory_order_consume约束符:在「当前线程」中,load操作之后的依赖于此原子变量的读和写操作都不能被重排到当前指令前。如果有「其他线程」使用memory_order_release内存模型对此原子变量进行store操作,在当前线程中是可见的。
Acquire-Release模型中的其它三个约束符,要么用来约束读,要么用来约束写。那么如何对一个原子操作中的两个动作执行约束呢?这就要用到 memory_order_acq_rel,它既可以约束读,也可以约束写。
对于使用memory_order_acq_rel约束符的原子操作,对当前线程的影响就是:当前线程T1中此操作之前或者之后的内存读写都不能被重新排序(假设此操作之前的操作为操作A,此操作为操作B,此操作之后的操作为C,那么执行顺序总是ABC,这块可以理解为同一线程内的sequenced-before关系);对其它线程T2的影响是,如果T2线程使用了memory_order_release约束符的写操作,那么T2线程中写操作之前的所有操作均对T1线程可见;如果T2线程使用了memory_order_acquire约束符的读操作,则T1线程的写操作对T2线程可见。
内存屏障的引入,本质上是由于CPU重排序指令引起的。内存屏障(Memory Barrier)是一种硬件或软件指令,用于控制处理器和内存系统中对内存操作的重新排序和优化。它们的作用是确保在屏障之前和之后的内存访问按照预期的顺序进行。
内存屏障主要有两种类型:读屏障(Read Barrier)和写屏障(Write Barrier)。
读屏障(也称为加载屏障):确保在读取一个变量的值之前,所有之前的读取操作和加载操作都已经完成。这可以防止读取过期的或无效的数据。
写屏障(也称为存储屏障):确保在写入一个变量的值之前,所有之前的写入操作和存储操作都已经完成。这可以防止将新的值预先存储到缓存而不是实际写入到内存中。
内存屏障的使用可以避免在多线程或并发环境下出现的一些问题,例如数据竞争、乱序执行和原子操作的正确性。通过插入内存屏障,可以使得代码在一个屏障之前或之后的内存访问按照预期的顺序执行,从而确保正确的内存可见性和一致性。
C++中内存屏障包括:std::atomic_thread_fence、std::atomic_signal_fence、std::thread::join。
创建一个内存屏障(memory barrier),用于*内存访问的重新排序和优化。它可以保证在屏障之前的所有内存操作都在屏障完成之前完成。常见的 memory_order 参数包括:
std::memory_order_relaxed:最轻量级的内存顺序,允许重排和优化。
std::memory_order_acquire:在屏障之前的内存读操作必须在屏障完成之前完成。
std::memory_order_release:在屏障之前的内存写操作必须在屏障完成之前完成。
std::memory_order_acq_rel:同时具有 acquire 和 release 语义,适用于同时进行读写操作的屏障。
std::memory_order_seq_cst:对于读操作相当于获得,对于写操作相当于释放。