从CPU缓存结构到原子操作

news2024/10/7 13:14:22

文章目录

  • 一、CPU缓存结构
    • 1.1 CPU的多级缓存
    • 1.2 Cache Line
  • 二、写回策略
  • 三、缓存一致性问题及解决方案
    • 3.1 缓存一致性问题
    • 3.2 解决方案
      • 3.2.1 总线嗅探
      • 3.2.2 事务的串行化
      • 3.2.3 MESI
  • 四、原子操作
    • 4.1 什么是原子操作
    • 4.2 c++ 标准库的原子类型
      • 4.2.1 atomic<T\>
      • 4.2.2 is_lock_free()
      • 4.2.3 load()
      • 4.2.4 store()
      • 4.2.5 exchange()
      • 4.2.6 compare_exchange_weak()
      • 4.2.7 compare_exchange_strong()
  • 五、内存序问题
    • 5.1 什么是内存序问题
    • 5.2 内存序
      • 5.2.1. memory_order_relaxed
      • 5.2.2. memory_order_release
      • 5.2.3. memory_order_acquire
      • 5.2.4. memory_order_acq_rel
      • 5.2.5. memory_order_seq_cst
    • 5.3 内存屏障
      • 5.3.1atomic_thread_fence()
  • 六、测试代码
    • 6.1 多线程加锁
    • 6.2 内存序问题
    • 6.3 多线程同步问题
      • 6.3.1 问题
      • 6.3.2 解法一:标志位
      • 6.3.3 解法二:互斥锁
      • 6.3.4 解法三:内存屏障

一、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),写回策略可以将多次对同一块数据的修改累积起来,一次性地写回主内存,减少了对主内存的访问,提高了效率。

三、缓存一致性问题及解决方案

3.1 缓存一致性问题

上面介绍的写回策略,延迟数据写入主内存的时机,可能会带来数据一致性的问题。因为CPU是多核的,在数据被修改后,尚未写回主内存之前,如果发生了缓存替换或其他操作,主内存上的数据可能是过期的。

比如在多线程的情况下,每个线程都有自己的缓存。假如线程 1 从主存中读取到 x,并对其加 1 ,此时还没有写回主存。与此同时,线程 2 也从主存中读取 x ,并加 1 。但是它们都不知道对方的存在,也不可以读取对方的缓存。若这时都将 x 写回主存,那此时 x 的值就少了 1 ,出现了数据不一致的问题。

3.2 解决方案

3.2.1 总线嗅探

当一个处理器执行一个写操作时,它会在总线上广播一个写请求,并在总线上传输要写入的数据。其他处理器的总线嗅探器会监听到该写请求,并检查请求中的地址。如果某个处理器的缓存中包含了该地址的数据块,总线嗅探器就会将该缓存块标记为“无效”,表示该数据已经过期,需要从主内存或其他缓存中重新获取最新的数据。

同样地,当一个处理器执行一个读操作时,总线嗅探器也会监听到该读请求,并检查请求中的地址。如果某个处理器的缓存中包含了该地址的数据块,总线嗅探器会检查该数据块是否为“脏”,即是否被修改过。如果是脏数据,则总线嗅探器负责将该数据写回到主内存或其他缓存中,以保证数据的一致性。

通过总线嗅探技术,处理器能够感知其他处理器对共享数据的读写操作,并及时更新自己的缓存,以确保所有处理器都能访问到最新的共享数据。

3.2.2 事务的串行化

举个例子,假设有两个事务 T1 和 T2,我们希望它们并发执行的过程如下:
T1:读取数据 A,修改数据 A(A = 10 ~> A = 20)
T2:读取数据 A,修改数据 A(A = 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、使用硬件原子指令:现代多核处理器通常支持硬件原子指令,例如CAS(Compare-And-Swap)指令。这样的指令允许对共享内存进行原子读取和写入操作。CAS指令会比较内存中的值与期望值,如果相等则执行写入操作,否则不执行。通过使用这样的原子指令,可以在多核处理器上实现原子操作。

2、使用锁和同步原语:多核处理器上的原子操作可以通过锁来实现互斥访问。以往0x86,是直接锁总线,避免所有内存的访问。现在是只需要锁住相关的内存,比较其他核心对这块内存的访问。

4.2 c++ 标准库的原子类型

4.2.1 atomic<T>

atomic<T> 是 C++ 中的原子类型模板,用于实现原子操作。它提供了一种线程安全的方式来对特定类型的数据进行读取和写入,以及执行其他常见的原子操作,如增加(增量)和交换等。

atomic<T> 提供了以下常用的成员函数和操作符:
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新值
a==b时:函数返回真,并把c赋值给a
a!=b时:函数返回假,并把a复制给b

#include <iostream>       // std::cout
#include <atomic>         // std::atomic, std::atomic_flag, ATOMIC_FLAG_INIT

int main()   //相等案例
{
    std::atomic<int> a;
    a.store(10);
    int b=10;  //a==b
    int c=20;
    std::cout<<"a:"<<a<<std::endl;
    while(!a.compare_exchange_weak(b,c))
    {
        b=10;
        c=20;
    } 
    std::cout<<"a true:"<<a.load()<<std::endl;
    std::cout<<"a:"<<a<<" b:"<<b<<" c:"<<c<<std::endl;
    return 0;
}


a:10
a true:20
a:20 b:10 c:20
int main()  //不等案例
{
    std::atomic<int> a;
    a.store(10);
    int b=100;  //a!=b
    int c=20;
    std::cout<<"a:"<<a<<std::endl;
    while( !a.compare_exchange_weak(b,c))
    {
        b=100;
        c=20;
    } 
    std::cout<<"a true:"<<a.load()<<std::endl;
    std::cout<<"a:"<<a<<" b:"<<b<<" c:"<<c<<std::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 i=10;
    int j=20;
    i+=2;
    j+=3;

我们以为执行顺序是从上往下,但编译器任务i和j没有关联,可能会优化成

    int i=10;
    i+=2;
    int j=20;
    j+=3;

在单核处理器的情况下,这种优化没问题,因为执行的结果不会变。但是如果是多核处理器,多线程并行执行,就会出现一些难以预知的问题。比如编译器和处理器可能会重排共享变量的写操作,使得其中一个线程的写操作先于另一个线程的写操作执行。这样会导致增量值丢失或重复计算,最终的结果可能小于预期的值。

总结来说,就是编译器和CPU会优化重排指令,改变原始程序中指令的执行顺序。这可能会导致多线程间的竞态条件和数据依赖关系出现问题,从而使得程序的行为产生难以预测的结果。

需要使用适当的内存序来指定对共享变量的读写操作的顺序和同步行为。

5.2 内存序

内存徐规定了多个线程访问同一个内存地址的语义,即
1)某个线程对内存地址的更新何时能被其他线程看见;
2)某个线程对内存地址访问附近可以做怎么样的优化;

5.2.1. memory_order_relaxed

松散内存序,只用来保证对原子对象的操作是原子的,在不需要保证顺序时使用。(保证原子性,不保证顺序性和同步性)
在这里插入图片描述

5.2.2. memory_order_release

释放操作,在写入某原子对象时,当前线程的任何前面的读写操作都不允许重排到这个操作的后面去,并且当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见。(保证原子性和同步性,顺序是当前线程的前面不能写到后面;但当前线程的后面可以写到前面)

在这里插入图片描述
在这里插入图片描述

5.2.3. memory_order_acquire

获得操作,在读取某原子对象时,当前线程的任何后面的读写操作都不允许重排到这个操作的前面去,并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见。(保证原子性和同步性,顺序是当前线程的后面不能写到前面;但当前线程的前面可以写到后面)
在这里插入图片描述

在这里插入图片描述

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 <mutex>
    std::mutex mtx;
    int count = 0;
#elif USE_SPINLOCK
    #include "spinlock.h"
    using spinlock_t = struct spinlock;
    spinlock_t spin;
    int count = 0;
#elif USE_ATOMIC
    #include <atomic>
    std::atomic<int> count{0};
#else
    int count = 0;
#endif


void incrby(int num) {
    for (int i=0; i < num; i++) {
#if USE_MUTEX
        mtx.lock();
        ++count;
        mtx.unlock();
#elif USE_SPINLOCK
        spinlock_lock(&spin);
        ++count;
        spinlock_unlock(&spin);
#elif USE_ATOMIC
        count.fetch_add(1);
#else
        ++count;
#endif
    }
}

int main() {
#ifdef USE_SPINLOCK
        spinlock_init(&spin);
#endif
    for (int i = 0; i < 100; i++) {
#ifdef USE_ATOMIC
        count.store(0);
#else
        count = 0;
#endif
        std::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_ATOMIC
        if (count.load() != 2000) {
#else
        if (count != 2000) {
#endif
            std::cout << "i:" << i << " count:" << count << std::endl;
            break;
        }
    }
    return 0;
}

6.2 内存序问题

1)实现功能:
初始x,y都为false;z为0;
线程a:先将x写为true,再将y写为true
线程b:自旋等待,知道读到的y为true,再判断达到x是否为true,若x为true,++z

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 <iostream>

std::atomic<bool> x,y;
std::atomic<int> 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被设置为true
    if(x.load(std::memory_order_relaxed))  // 4
        ++z;
}

int main()
{
    x=false;
    y=false;
    z=0;
    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 <iostream>

std::atomic<int> 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 <iostream>

std::atomic<int> x(0);
std::atomic<bool> 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 <mutex>

std::atomic<int> 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_guard<std::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 <iostream>

std::atomic<int> 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;
}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/726774.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件安全测试流程与方法分享(上)

安全测试是在IT软件产品的生命周期中&#xff0c;特别是产品开发基本完成到发布阶段&#xff0c;对产品进行检验以验证产品符合安全需求定义和产品质量标准的过程。安全是软件产品的一个重要特性&#xff0c;安全测试也是软件测试重的一个重要类别&#xff0c;本系列文章我们与…

MySQL简单查询操作

系列文章目录 前言SELECT子句SELECT后面之间跟列名DISTINCT,ALL列表达式列更名 WHERE子句WHERE子句中可以使用的查询条件比较运算特殊比较运算符BETWEEN...AND...集合查询&#xff1a;IN模糊查询&#xff1a;LIKE空值比较&#xff1a;IS NULL 多重条件查询 ORDER BY子句排序复杂…

线性规划解的概念

一、线性规划的可行解 若x1,x2满足条件[1]-[4],则称向量为线性规划问题的一个可行解。 例如 其中x(1),x(2)为可行解&#xff0c;而x(3),x(4)不是可行解。 二、线性规划的可行域 所有可行解构成的集合称为该线性规划的可行域。 三、线性规划的最优解 使目标函数最大或最小的…

Git ① 通过git将本地两个项目进行合并

一、新建一个本地仓库 ① 新建一个文件夹&#xff0c;打开之后在命令行输入git init 初始化仓库。 git init ② 在新建的文件夹中随便创建一个文件&#xff08;这样才能新建新的分支&#xff0c;不然新建分支命令没有作用&#xff09; ③ 输入命令 git add . 和 git commit…

如何实现对视频录像文件的AI算法分析?

有用户提出需求&#xff0c;提供视频文件给平台&#xff0c;并进行AI算法分析。值得一提的是&#xff0c;我们的平台不仅仅可以基于AI算法&#xff0c;对设备实时传输的视频流进行分析&#xff0c;也能对视频回放录像文件进行智能分析。那么是如何实现的呢&#xff1f; EasyDSS…

Linux 共享内存

概念&#xff1a; 在Linux系统中&#xff0c;共享内存是一种用于进程间通信的机制&#xff0c;它允许多个进程共享同一块内存区域。 Linux 共享内存的作用和目的&#xff1a; Linux共享内存的主要目的是在不同的进程之间实现高效的数据交换和共享。它可以用于以下几个方面&…

在uniapp 小程序 vue中报 错 Cannot read property ‘substring‘ of undefined

是因 是因为对字符串使用substring的时候页面中的数据还没有加载 。 错误代码&#xff1a; 可以使用 v-if 修改为&#xff1a;

Alibaba官方上线!SpringBoot+SpringCloud全彩指南(终极版)

Alibaba作为国内一线互联网大厂&#xff0c;其中SpringCloudAlibaba更是阿里微服务最具代表性的技术之一&#xff0c;很多人只知道SpringCloudAlibaba其实面向微服务技术基本上都有的下面就给大家推荐一份Alibaba官网最新版&#xff1a;SpringSpringBootSpringCloud微服务全栈开…

2023 WAIC | 自然机器人向全球传递新一代智能自动化之声

2023年7月6日-7月8日&#xff0c;备受瞩目的“2023世界人工智能大会”在上海世博中心及世博展览馆隆重召开&#xff0c;本届大会的主题是“智联世界&#xff0c;生成未来”&#xff0c;大会由上海市人民政府和国家发改委、工信部、科技部、国家网信办、中国科学院、中国工程院、…

JavaScript实现归并排序算法详解

JavaScript实现归并排序算法详解 说明 归并排序&#xff08;Merge Sort&#xff09;算法&#xff0c;也叫合并排序&#xff0c;是创建在归并操作上的一种有效的排序算法。算法是采用分治法&#xff08;Divide and Conquer&#xff09;的一个非常典型的应用&#xff0c;且各层…

Talk预告 | 南洋理工大学助理教授潘新钢:拖动你的GAN - 在生成图像流形上基于控制点的交互式图像编辑

本期为TechBeat人工智能社区第511期线上Talk&#xff01; 北京时间7月6日(周四)20:00&#xff0c;南洋理工大学 助理教授—潘新钢的Talk将准时在TechBeat人工智能社区开播&#xff01; 他与大家分享的主题是: “拖动你的GAN - 在生成图像流形上基于控制点的交互式图像编辑”&…

嵌入式_Keil (MDK - ARM) 的调试步骤

目录 1. 编译 调试 2. 复位 全速运行 3. 单步调试 4. 逐步调试 5. 跳出调试 6. 运行到光标处 7. 跳转到暂停行 8. 调试窗口 首先为什么需要在 MDK 中进行程序的调试呢&#xff1f; 在 MDK 中进行程序调试的主要目的是识别和解决程序中的问题和错误。 比如说找到程序中…

五种网络 I/O 模型

文章目录 1. 阻塞式 I/O 模型2. 非阻塞式 I/O 模型3. I/O 多路复用4. 信号驱动式 I/O5. 异步 I/O6. I/O 模型的分类 Unix 下有五种可用的 I/O 模型&#xff1a; 阻塞式 I/O 非阻塞式 I/O I/O 多路复用&#xff08;select/poll/epoll&#xff09; 信号驱动式 I/O&#xff08;…

【Spring MVC】Spring MVC程序开发教程:常见的注解及使用方式详情

前言 Spring MVC是一种常用的Web框架&#xff0c;它可以帮助开发人员快速构建可扩展的Web应用程序。为了提供更好的开发体验和更高的代码效率&#xff0c;Spring MVC提供了各种注解。这些注解可以用于控制器、请求参数、响应类型、表单数据验证、异常处理等方面。在本文中&…

卡尔曼滤波(附C++代码)

是什么 任何时候对于动态系统存在不确定信息&#xff0c;都可使用卡尔曼滤波&#xff08;Kalman Filter&#xff0c;下面简称为KF&#xff09;对系统下一步要做什么做出有根据的猜测。 KF对于连续变化的系统是理想的&#xff0c;优点是占用内存小而且速度快&#xff0c;非常适…

在Linux中安装RabbitMQ

RabbitMQ下载网址 Socat下载网址 erlang下载网址 RabbitMQ安装包依赖于Erlang语言包的支持&#xff0c;所以需要先安装Erlang语言包&#xff0c;再安装RabbitMQ安装包 通过Xftp软件将这三个压缩包上传到linux中的opt目录下 ,双击即可 在安装之前先查询…

Android oss policy上传

OSS Policy方式上传 一、 流程对比1.1 普通上传1.2 服务端签名后直传 二、获取上传的policy签名配置三、请求OSS上传文件四、调用应用服务器接口同步文件五、关于上传OSS报错注意事项六、附送链接 一、 流程对比 1.1 普通上传 缺点&#xff1a; 上传慢&#xff1a;用户数据需…

数学建模常用模型(五):多元回归模型

数学建模常用模型&#xff08;五&#xff09;&#xff1a;多元回归模型 由于客观事物内部规律的复杂性及人们认识程度的限制&#xff0c;无法分析实际对象内在的因果关系&#xff0c;建立合乎机理规律的数学模型。所以在遇到有些无法用机理分析建立数学模型的时候&#xff0c;…

docker中运行RabbitMq的启用插件指南

我们使用 Docker 来运行 RabbitMQ&#xff0c;有时需要启用一些插件&#xff0c;这个与正常安装的启用插件的步骤会有所不同。以下是在 Docker 中启用 RabbitMQ 插件的一般步骤&#xff1a; 首先&#xff0c;确认已经将 rabbitmq_delayed_message_exchange-3.12.0.ez 插件文件复…

raid5两块磁盘掉线导致阵列崩溃的服务器数据恢复案例

服务器数据恢复环境&#xff1a; DELL PowerVault系列某型号存储&#xff0c;15块硬盘搭建了一组RAID5磁盘阵列。 服务器故障&检测&#xff1a; 存储设备raid5阵列中一块磁盘由于未知原因离线&#xff0c;管理员对该磁盘阵列进行了同步操作。在同步的过程中又有一块磁盘指示…