恩施北京网站建设,南昌网站建设方案开发,营销推广的特点,元氏网站制作一、CPU缓存结构
1.1 CPU的多级缓存
因为CPU的计算速度非常快#xff0c;但内存的访问速度相对较慢。因此#xff0c;如果CPU每次都要从内存读取数据#xff0c;会造成大量的等待时间#xff0c;降低整体性能。
通过引入多级缓存#xff0c;可以在CPU和内存之间建立数据…一、CPU缓存结构
1.1 CPU的多级缓存
因为CPU的计算速度非常快但内存的访问速度相对较慢。因此如果CPU每次都要从内存读取数据会造成大量的等待时间降低整体性能。
通过引入多级缓存可以在CPU和内存之间建立数据缓存层将最常用的数据暂时保存在靠近CPU的高速缓存CPU Cache中以供CPU快速访问。不同级别的缓存容量和访问速度各不相同一般来说L1缓存最小、速度最快L2缓存次之L3缓存最大但速度相对较慢。并且 L1和L2 是 CPU 私有L3 是所有 CPU 共享。 多级缓存的设计可以实现更高的命中率即CPU能够更频繁地从高速缓存中获取需要的数据减少对内存的访问次数从而提高整体性能。
1.2 Cache Line
CPU 从内存中读取数据到 CPU Cache 的过程中是一小块一小块来读取数据的。这样一小块一小块的数据在 CPU Cache 里面我们把它叫作缓存行(Cache Line)。即Cache Line是CPU缓存中的最小可读写单元用于存储从主存中读取的数据块。日常使用的 Intel 服务器或者 PC 里Cache Line 的大小通常是 64 字节。
当CPU访问内存时如果所需数据在缓存中已经存在于一个Cache Line中那么CPU可以直接从缓存中读取数据而无需访问主存从而提高了数据传输的速度。 标志位flag用于指示Cache Line当前是否有效。当一个Cache Line中存储的数据被更新或替换时标志位会被清除表示该Cache Line不再有效。存MESI 的状态 标记tag用于标识数据区域中存储的数据块是来自哪个主存地址。当CPU需要读取或写入特定地址的数据时它会将该地址的一部分作为标记并与Cache Line中存储的标记进行比较以确定是否命中缓存。 数据区域data用于存储从主存中读取的数据块。
二、写回策略
写回策略Write Back是一种用于缓存管理的写入更新方式。当数据被修改时写回策略将更新后的数据首先写入缓存而不立即写入主内存。
具体来说当缓存中的某个数据被修改时写回策略将在修改时更新缓存中的对应数据并将其标记为脏数据表示该数据已经被修改过。然后当需要替换这个被修改的数据时才将更新后的数据写回主内存。 当请求是写请求时
1若命中直接将新数据写入缓存并且标记为脏数据dirty缓存中修改过但尚未写回到更高级别缓存或主内存中的数据。注意此时不会写入内存。
2若未命中分配一个缓存块Cache Line判断当前缓存块是不是脏数据。如果是先将缓存块的数据写回内存中再将新数据写入缓存块。如果不是脏数据直接从内存中读到缓存块中建立内存块与缓存块的索引关系再将新数据写入缓存块并标记为dirty。 当请求是读请求时
1若命中直接返回其数据
2若未命中时分配一个缓存块判断当前缓存块是不是脏数据。如果是先将缓存块的数据写回下一级存储中再从内存读取新数据到缓存块中。如果不是脏数据直接从内存中读到缓存块中修改dirty位为clean未被修改。最后返回数据。 这种策略的主要优势在于减少了向主内存写入数据的次数。相比于每次数据修改都直接写入主内存写直达Write Through写回策略可以将多次对同一块数据的修改累积起来一次性地写回主内存减少了对主内存的访问提高了效率。
相关视频推荐
2024年c/c程序员如何提升自己的核心竞争力这套linux c/c后端服务器开发技术教程不要错过https://www.bilibili.com/video/BV1CF4m1L7hU/
免费学习地址Linux C/C开发后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全
需要C/C Linux服务器架构师学习资料加qun579733396获取资料包括C/CLinuxgolang技术NginxZeroMQMySQLRedisfastdfsMongoDBZK流媒体CDNP2PK8SDockerTCP/IP协程DPDKffmpeg等免费分享 三、缓存一致性问题及解决方案
3.1 缓存一致性问题
上面介绍的写回策略延迟数据写入主内存的时机可能会带来数据一致性的问题。因为CPU是多核的在数据被修改后尚未写回主内存之前如果发生了缓存替换或其他操作主内存上的数据可能是过期的。
比如在多处理器系统下核心A和核心A共享一块主存。假如核心A从主存中读取到 x并对其加 1 此时还没有写回主存。与此同时核心B 也从主存中读取 x 并加 1 。但是它们都不知道对方的存在也不可以读取对方的缓存。若这时都将 x 写回主存那此时 x 的值就少了 1 出现了数据不一致的问题。
3.2 解决方案
3.2.1 总线嗅探
当一个处理器执行一个写操作时它会在总线上广播一个写请求并在总线上传输要写入的数据。其他处理器的总线嗅探器会监听到该写请求并检查请求中的地址。如果某个处理器的缓存中包含了该地址的数据块总线嗅探器就会将该缓存块标记为“无效”表示该数据已经过期需要从主内存或其他缓存中重新获取最新的数据。
同样地当一个处理器执行一个读操作时总线嗅探器也会监听到该读请求并检查请求中的地址。如果某个处理器的缓存中包含了该地址的数据块总线嗅探器会检查该数据块是否为“脏”即是否被修改过。如果是脏数据则总线嗅探器负责将该数据写回到主内存或其他缓存中以保证数据的一致性。
通过总线嗅探技术处理器能够感知其他处理器对共享数据的读写操作并及时更新自己的缓存以确保所有处理器都能访问到最新的共享数据。
3.2.2 事务的串行化
举个例子假设有两个事务 T1 和 T2我们希望它们并发执行的过程如下
T1读取数据 A修改数据 AA 10 ~ A 20
T2读取数据 A修改数据 AA 20 ~ A 30
但是如果这两个事务并发执行并未经过任何的串行化控制可能出现以下情况
T1 先执行读取操作读取到 A 的值为 10
T2 在 T1 执行读取操作后执行读取操作读取到 A 的值也为 10
T1 执行修改操作将 A 的值修改为 20
T2 也执行修改操作将 A 的值修改为 30
在这种情况下最终 A 的值是 30而不是按照顺序执行时的 20。
然而如果采用串行化控制将 T1 和 T2 串行化执行保证它们不会交叉执行那么最终的结果将与串行执行的结果一致。具体的串行化执行过程如下
T1 先执行读取操作读取到 A 的值为 10
T1 执行修改操作将 A 的值修改为 20
T2 在 T1 执行完毕后执行读取操作读取到 A 的值为 20
T2 执行修改操作将 A 的值修改为 30
采用串行化控制后最终 A 的值为 30与串行执行的结果一致。
可以通过对线程T1加锁保证T1执行时候T2不会产生干扰达到串行效果。
3.2.3 MESI
通过事务的串行化每当有核心修改数据都需要广播给其他的核心。但是并不是所有的核心都与这个数据相关。这样就会浪费带宽代价比较大。接下来引入MESI来解决这个问题。
MESI是一种缓存一致性协议用于解决多核处理器中的缓存一致性问题。CPU 中每个缓存行caceh line都使用MESI进行标记MESI是四种状态的缩写分别代表了缓存行的不同状态修改Modified、独占Exclusive、共享Shared和无效Invalid。
1修改Modified当某个核心独占地拥有一块缓存行的数据时如果该核心对缓存行进行了修改那么该缓存行的状态为修改状态。同时该核心对缓存行所做的修改还没有写回到主存中。
2独占Exclusive如果某个核心独占地拥有一块缓存行的数据并且该数据未被修改那么该缓存行的状态为独占状态。此时其他核心不能缓存该缓存行中的数据。
3共享Shared当多个核心同时缓存同一块缓存行的数据时该缓存行的状态为共享状态。多个核心可以同时读取该数据但不能进行写操作。
4无效Invalid如果某个核心的缓存中的数据与主存中的数据不一致或者某个核心将共享缓存行标记为无效状态那么该缓存行的状态就会变为无效状态。此时其他核心不能使用该缓存行中的数据必须从主存中获取最新的数据。
四、原子操作
4.1 什么是原子操作
原子操作是指在执行过程中不会被中断的操作要么全部执行成功要么全部不执行不会出现部分执行的情况。原子操作可以看作是不可分割的单元 运行期间不会有任何的上下文切换。
1在单核处理器上原子操作可以通过禁止中断的方式来保证不被中断。当一个线程或进程执行原子操作时可以通过禁用中断来确保原子性。在禁用中断期间其他线程或进程无法打断当前线程或进程的执行从而保证原子操作的完整性。
2在多核处理器上原子操作的实现需要使用一些特殊的硬件机制或同步原语来保证原子性。以下是两种常见的方法
1、使用硬件原子指令现代多核处理器通常支持硬件原子指令例如CASCompare-And-Swap指令。这样的指令允许对共享内存进行原子读取和写入操作。CAS指令会比较内存中的值与期望值如果相等则执行写入操作否则不执行。通过使用这样的原子指令可以在多核处理器上实现原子操作。
2、使用锁和同步原语多核处理器上的原子操作可以通过锁来实现互斥访问。以往0x86是直接锁总线避免所有内存的访问。现在是只需要锁住相关的内存比较其他核心对这块内存的访问。
4.2 c 标准库的原子类型
4.2.1 atomicT
atomicT 是 C 中的原子类型模板用于实现原子操作。它提供了一种线程安全的方式来对特定类型的数据进行读取和写入以及执行其他常见的原子操作如增加增量和交换等。
atomicT 提供了以下常用的成员函数和操作符
1加载和存储操作通过 load() 和 store() 方法可以实现从原子对象中加载值或将值存储到原子对象中。
2交换和比较交换操作使用 exchange() 可以原子地将新值存储到原子对象中并返回之前的值使用 compare_exchange_strong() 或 compare_exchange_weak() 可以原子地比较并交换值。
3增减操作使用 fetch_add() 和 fetch_sub() 可以原子地增加或减少原子对象的值并返回之前的值。
4访问操作除了上述操作还可以使用 operator、operator–、operator、operator- 等操作符进行原子操作。
4.2.2 is_lock_free()
用于检查原子类型是否是无锁lock-free的。它返回一个布尔值指示原子类型是否可以在特定硬件平台上以无锁方式进行操作。
bool is_lock_free() const noexcept;
如果 is_lock_free() 返回 true表示该原子类型可以在特定硬件平台上以无锁方式进行操作如果返回 false则表示该原子类型无法以无锁方式进行操作需要使用锁或其他同步机制。
4.2.3 load()
获取原子对象中的当前值并返回该值。它会保证在多线程环境下对数据的读取是原子的即不会受到其他线程同时修改的干扰保证了数据的一致性。
load(memory_order order memory_order_seq_cst) const noexcept;
参数 order 是一个可选参数用于指定内存序memory order的类型默认为 memory_order_seq_cst。内存序定义了原子操作的时序关系决定了在多线程环境下对数据访问的可见性和有序性。
4.2.4 store()
用于原子地存储写入值到原子对象中。它可以将给定的值存储到原子对象中并保证在多线程环境下的可见性和原子性。
void store(T value, memory_order order memory_order_seq_cst) noexcept;
参数 value 是要存储到原子对象中的值参数 order 是一个可选参数用于指定内存序memory order的类型默认为 memory_order_seq_cst。
4.2.5 exchange()
用于原子地交换原子对象中的值并返回先前的值。它可以将给定的值与原子对象的当前值进行交换并保证在多线程环境下的可见性和原子性。
exchange(T desired, memory_order order memory_order_seq_cst) noexcept;
参数 desired 是要与原子对象进行交换的值参数 order 是一个可选参数用于指定内存序memory order的类型默认为 memory_order_seq_cst。
4.2.6 compare_exchange_weak()
用于原子地比较并交换原子对象的值。它可以比较原子对象的当前值与期望值并在匹配时将新值存储到原子对象中。
bool compare_exchange_weak(T expected, T desired,memory_order success, memory_order failure) noexcept;
参数 expected 是对原子对象进行比较的期望值并且在返回时被更新为原子对象的当前值。参数 desired 是要存储到原子对象中的新值。参数 success 和 failure 分别指定了成功和失败情况下的内存序memory order类型默认为 memory_order_seq_cst。
返回值是一个 bool 类型表示是否成功执行了比较和交换操作。如果比较的值与期望值相等则交换成功返回 true否则交换失败返回 false。
weak版本的CAS允许偶然出乎意料的返回比如在字段值和期待值一样的时候却返回了false不过在一些循环算法中这是可以接受的。通常它比起strong有更高的性能。
举个例子
a.compare_exchange_weak(b,c)其中a是当前值b期望值c新值
ab时函数返回真并把c赋值给a
a!b时函数返回假并把a复制给b
#include iostream // std::cout
#include atomic // std::atomic, std::atomic_flag, ATOMIC_FLAG_INITint main() //相等案例
{std::atomicint a;a.store(10);int b10; //abint c20;std::couta:astd::endl;while(!a.compare_exchange_weak(b,c)){b10;c20;} std::couta true:a.load()std::endl;std::couta:a b:b c:cstd::endl;return 0;
}a:10
a true:20
a:20 b:10 c:20 int main() //不等案例
{std::atomicint a;a.store(10);int b100; //a!bint c20;std::couta:astd::endl;while( !a.compare_exchange_weak(b,c)){b100;c20;} std::couta true:a.load()std::endl;std::couta:a b:b c:cstd::endl;return 0;
}
a:10
a:10 b:10 c:20
4.2.7 compare_exchange_strong()
强化版的CAS如果需要保证严格的原子性则应该使用 compare_exchange_strong 函数。其他根weak版一样。
compare_exchange_strong ------------ 会阻塞cpu, 会慢一些
compare_exchange_weak --------------- 有可能失败性能高, 可以加while直到它成功
五、内存序问题
5.1 什么是内存序问题
内存序memory order问题是由于多线程的并行执行可能导致的对共享变量的读写操作无法按照程序员预期的顺序进行。
简单来说编译器为了提高运算速度有时候会做出违背代码原有顺序的优化。虽然顺序改变了但执行的结果不会变。比如下面一段代码 int i10;int j20;i2;j3;
我们以为执行顺序是从上往下但编译器任务i和j没有关联可能会优化成 int i10;i2;int j20;j3;
在单核处理器的情况下这种优化没问题因为执行的结果不会变。但是如果是多核处理器多线程并行执行就会出现一些难以预知的问题。比如编译器和处理器可能会重排共享变量的写操作使得其中一个线程的写操作先于另一个线程的写操作执行。这样会导致增量值丢失或重复计算最终的结果可能小于预期的值。
总结来说就是编译器和CPU会优化重排指令改变原始程序中指令的执行顺序。这可能会导致多线程间的竞态条件和数据依赖关系出现问题从而使得程序的行为产生难以预测的结果。
需要使用适当的内存序来指定对共享变量的读写操作的顺序和同步行为。
5.2 内存序
内存徐规定了多个线程访问同一个内存地址的语义即 1某个线程对内存地址的更新何时能被其他线程看见 2某个线程对内存地址访问附近可以做怎么样的优化
5.2.1. memory_order_relaxed
松散内存序只用来保证对原子对象的操作是原子的在不需要保证顺序时使用。保证原子性不保证顺序性和同步性 5.2.2. memory_order_release
释放操作在写入某原子对象时当前线程的任何前面的读写操作都不允许重排到这个操作的后面去并且当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见。保证原子性和同步性顺序是当前线程的前面不能写到后面但当前线程的后面可以写到前面 添加图片注释不超过 140 字可选
5.2.3. memory_order_acquire
获得操作在读取某原子对象时当前线程的任何后面的读写操作都不允许重排到这个操作的前面去并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见。保证原子性和同步性顺序是当前线程的后面不能写到前面但当前线程的前面可以写到后面 添加图片注释不超过 140 字可选 5.2.4. memory_order_acq_rel
获得释放操作一个读‐修改‐写操作同时具有获得语义和释放语义即它前后的任何读写操作都不允许重排并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见
5.2.5. memory_order_seq_cst
顺序一致性语义对于读操作相当于获得对于写操作相当于释放对于读‐修改‐写操作相当于获得释放是所有原子操作的默认内存序并且会对所有使用此模型的原子操作建立一个全局顺序保证了多个原子变量的操作在所有线程里观察到的操作顺序相同当然它是最慢的同步模型。
5.3 内存屏障
内存屏障Memory Barrier是一种硬件或软件指令用于控制处理器和内存系统中对内存操作的重新排序和优化。它们的作用是确保在屏障之前和之后的内存访问按照预期的顺序进行。
内存屏障主要有两种类型读屏障Read Barrier和写屏障Write Barrier。
读屏障也称为加载屏障确保在读取一个变量的值之前所有之前的读取操作和加载操作都已经完成。这可以防止读取过期的或无效的数据。
写屏障也称为存储屏障确保在写入一个变量的值之前所有之前的写入操作和存储操作都已经完成。这可以防止将新的值预先存储到缓存而不是实际写入到内存中。
内存屏障的使用可以避免在多线程或并发环境下出现的一些问题例如数据竞争、乱序执行和原子操作的正确性。通过插入内存屏障可以使得代码在一个屏障之前或之后的内存访问按照预期的顺序执行从而确保正确的内存可见性和一致性。
5.3.1atomic_thread_fence()
创建一个内存屏障memory barrier用于限制内存访问的重新排序和优化。它可以保证在屏障之前的所有内存操作都在屏障完成之前完成。
void atomic_thread_fence(std::memory_order order);
常见的 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对于读操作相当于获得对于写操作相当于释放。
六、测试代码
6.1 多线程加锁
四个线程每个线程实现count 500次。最后结果应该是cout 2000 在没有加锁的情况下会出现cout ≠ 2000 #include chrono
#include iostream
#include thread
#include assert.h#define USE_ATOMIC 1#if USE_MUTEX#include mutexstd::mutex mtx;int count 0;
#elif USE_SPINLOCK#include spinlock.husing spinlock_t struct spinlock;spinlock_t spin;int count 0;
#elif USE_ATOMIC#include atomicstd::atomicint count{0};
#elseint count 0;
#endifvoid incrby(int num) {for (int i0; i num; i) {
#if USE_MUTEXmtx.lock();count;mtx.unlock();
#elif USE_SPINLOCKspinlock_lock(spin);count;spinlock_unlock(spin);
#elif USE_ATOMICcount.fetch_add(1);
#elsecount;
#endif}
}int main() {
#ifdef USE_SPINLOCKspinlock_init(spin);
#endiffor (int i 0; i 100; i) {
#ifdef USE_ATOMICcount.store(0);
#elsecount 0;
#endifstd::thread a(incrby, 500);std::thread b(incrby, 500);std::thread c(incrby, 500);std::thread d(incrby, 500);a.join();b.join();c.join();d.join();
#ifdef USE_ATOMICif (count.load() ! 2000) {
#elseif (count ! 2000) {
#endifstd::cout i: i count: count std::endl;break;}}return 0;
}
6.2 内存序问题
1实现功能
初始xy都为falsez为0
线程a先将x写为true再将y写为true
线程b自旋等待知道读到的y为true再判断达到x是否为true若x为truez
2若函数write_x_then_y()和read_y_then_x()的内存序如下 则在极少数情况下结果z不为1.
这是因为memory_order_relaxed不保证执行顺序因此编译器和处理器可能会优化重排。此时会出现一种执行顺序4~ 1 ~ 2 ~ 3 。
这就是线程b中对共享变量y的读操作先于线程a对y的写操作执行。这样会导致某个线程读取到较旧的值而不是最新的值影响最终结果的准确性。
3若函数write_x_then_y()和read_y_then_x()的内存序如下 这时结果z一定为1。
这是因为内存序memory_order_release有顺序性保守当前线程前面的操作不能优化到后面即1一定在2前面。并且还有同步性即2若执行了1一定执行了。
内存序memory_order_acquire有顺序性保守当前线程后面的操作不能优化到前面即3一定在4前面.
#include atomic
#include thread
#include assert.h
#include iostreamstd::atomicbool x,y;
std::atomicint z;void write_x_then_y()
{x.store(true,std::memory_order_relaxed); // 1 y.store(true,std::memory_order_release); // 2 y true x true
}void read_y_then_x()
{while(!y.load(std::memory_order_acquire)); // 3 自旋等待y被设置为trueif(x.load(std::memory_order_relaxed)) // 4z;
}int main()
{xfalse;yfalse;z0;std::thread a(write_x_then_y);std::thread b(read_y_then_x);a.join();b.join();std::cout z.load(std::memory_order_relaxed) std::endl;return 0;
}
6.3 多线程同步问题
实现功能
创建2个线程线程t1执行i从0加到9999结果按原子操作写入x线程t2执行i从0减到-9999结果按原子操作写入x最后打印出x的值。
6.3.1 问题
代码1的执行结果如图结果出现错乱。有时候是9999有时候是-9999。这是因为两个线程将结果存入主存的顺序是不确定的。 代码1
#include atomic
#include thread
#include iostreamstd::atomicint x(0);void thread_func1()
{for (int i 0; i 100000; i){x.store(i, std::memory_order_relaxed);}
}void thread_func2()
{for (int i 0; i 100000; i){x.store(-i, std::memory_order_relaxed);}
}int main()
{std::thread t1(thread_func1);std::thread t2(thread_func2);t1.join();t2.join();std::cout Final value of x x.load(std::memory_order_relaxed) std::endl;return 0;
}
6.3.2 解法一标志位
代码2通过添加一个标志位来控制线程t1的执行 代码2
#include atomic
#include thread
#include iostreamstd::atomicint x(0);
std::atomicbool t1_finished(false); // 标志位表示线程t1是否已完成void thread_func1()
{for (int i 0; i 100000; i){x.store(i, std::memory_order_release);}t1_finished.store(true, std::memory_order_release); // 在线程t1完成后设置标志位为true
}void thread_func2()
{while (!t1_finished.load(std::memory_order_acquire)) // 检查标志位如果线程t1未完成则等待{std::this_thread::yield();}for (int i 0; i 100000; i){x.store(-i, std::memory_order_release);}
}int main()
{std::thread t1(thread_func1);std::thread t2(thread_func2);t1.join();t2.join();std::cout Final value of x x.load(std::memory_order_acquire) std::endl;return 0;
}
6.3.3 解法二互斥锁
代码3在线程t2中每次修改x之前使用了std::lock_guardstd::mutex来自动加锁以确保线程t2的操作在同一时刻只有一个线程进行。这样就能够保证最后只输出线程t2的结果。
代码3
#include atomic
#include thread
#include iostream
#include mutexstd::atomicint x(0);
std::mutex mtx; // 互斥锁void thread_func1()
{for (int i 0; i 100000; i){x.store(i, std::memory_order_relaxed);}
}void thread_func2()
{for (int i 0; i 100000; i){std::lock_guardstd::mutex lock(mtx); // 加锁x.store(-i, std::memory_order_relaxed);}
}int main()
{std::thread t1(thread_func1);std::thread t2(thread_func2);t1.join();t2.join();std::cout Final value of x x.load(std::memory_order_acquire) std::endl;return 0;
}
6.3.4 解法三内存屏障
在线程t2的循环中插入了std::atomic_thread_fence(std::memory_order_release)内存屏障指令。该指令用于确保在执行x.store(-i, std::memory_order_relaxed)之前的所有先行写操作对于其它线程的读操作都可见。这样我们就能够保证最终只输出线程t2的结果。
#include atomic
#include thread
#include iostreamstd::atomicint x(0);void thread_func1()
{for (int i 0; i 100000; i){x.store(i, std::memory_order_release);}
}void thread_func2()
{for (int i 0; i 100000; i){x.store(-i, std::memory_order_release);std::atomic_thread_fence(std::memory_order_release); // 插入内存屏障}
}int main()
{std::thread t1(thread_func1);std::thread t2(thread_func2);t1.join();t2.join();std::cout Final value of x x.load(std::memory_order_acquire) std::endl;return 0;
}