2024年C C++最新C++并发编程之四 内存模型和原子操作_原子 内存模型,2024年最新看完这一篇你就懂了

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

std::atomic: 无符号长整型原子变量,支持原子读/写操作。
std::atomic: 长长整型原子变量,支持原子读/写操作。
std::atomic: 无符号长长整型原子变量,支持原子读/写操作。
std::atomic: 单精度浮点型原子变量,支持原子读/写操作。
std::atomic: 双精度浮点型原子变量,支持原子读/写操作。
std::atomic: 长双精度浮点型原子变量,支持原子读/写操作。
std::atomic<void*>: 指针类型原子变量,支持原子读/写操作。
以上的原子类型都有相应的成员函数,例如 load(), store(), exchange() 等,用于进行原子操作。

需要注意的是,std::atomic类型的对象一般是被声明为全局变量或静态变量,以便在多个线程间进行共享访问。在使用原子类型时,还需要注意保证操作的原子性,以避免竞争条件的出现。

大部分原子类型都有一个共同成员函数:is_lock_free()

  1. 该函数主要用于检查一个原子类型是否支持无锁操作(lock-free operation)。
  2. 所谓无锁操作是指一个原子类型的操作可以在不使用互斥锁的情况下进行,通常使用CPU的硬件支持来实现。在支持无锁操作的平台上,原子类型的操作可以比使用互斥锁更高效地进行。
  3. is_lock_free()成员函数返回一个bool值,表示一个原子类型是否支持无锁操作。如果返回true,则表示该原子类型支持无锁操作;如果返回false,则表示该原子类型不支持无锁操作。
  4. 需要注意的是,is_lock_free()成员函数的返回值不保证在所有平台上都相同,因此在编写跨平台代码时,需要谨慎使用该函数的返回值。此外,即使一个原子类型支持无锁操作,使用互斥锁进行操作仍然是一种安全的做法。

当使用std::atomic<>泛化模板时,其具备的操作包括:

  1. 加载(load)操作:用于从原子对象中读取当前值并返回。可以使用std::memory_order参数指定内存序(memory order)来控制原子操作的同步方式。
  2. 存储(store)操作:用于将给定的值存储到原子对象中。同样可以使用std::memory_order参数控制同步方式。
  3. 交换(exchange)操作:用于原子地交换原子对象中的值和给定的值,并返回原来的值。也可以使用std::memory_order参数来指定同步方式。
  4. 比较并交换(compare_and_exchange, CAS)操作:用于原子地比较原子对象的当前值和期望值,如果相等则将新值存储到原子对象中。CAS操作可以帮助解决多线程编程中的竞争条件。也可以使用std::memory_order参数来指定同步方式。
  5. 原子操作(compare_exchange_strong, compare_exchange_weak):与CAS操作类似,不过在比较和交换期间可能进行额外的检查,以避免不必要的CAS操作。compare_exchange_strong()保证CAS操作的强一致性,而compare_exchange_weak()则提供了弱一致性的保证。
  6. 原子递增(increment)和递减(decrement)操作:用于原子地增加或减少原子对象的值。也可以使用std::memory_order参数来指定同步方式。

除此之外,std::atomic<>泛化模板还支持一些其他的操作,如fetch_add、fetch_sub、fetch_and、fetch_or、fetch_xor等,这些操作可以原子地执行加法、减法、按位与、按位或和按位异或等运算,并返回操作前的值。同样可以使用std::memory_order参数来指定同步方式。

当使用原子类型进行多线程同步时,需要使用内存模型来指定操作之间的顺序关系。C++11 提供了五种内存模型,分别是 memory_order_relaxed、memory_order_acquire、memory_order_release、memory_order_acq_rel 和 memory_order_seq_cst。

  1. memory_order_relaxed
    该内存模型是最弱的一种内存模型。使用该内存模型,不保证原子操作的顺序和其他线程所见到的顺序一致,也不保证操作的可见性。该内存模型可以提高程序的性能,但是需要开发者自行处理数据的一致性。
  2. memory_order_acquire
    该内存模型用于读取操作,可以保证在读取 atomic 变量之前的所有内存访问都被完成。也就是说,其他线程对 atomic 变量的写入必须在当前线程进行读取之前完成,否则当前线程可能看不到最新的数据。
  3. memory_order_release
    该内存模型用于写入操作,可以保证在写入 atomic 变量之后的所有内存访问都被完成。也就是说,当前线程对 atomic 变量的写入必须在其他线程进行读取之前完成,否则其他线程可能看不到最新的数据。
  4. memory_order_acq_rel
    该内存模型是 memory_order_acquire 和 memory_order_release 的组合,即同时包含读取和写入的同步机制。这种内存模型可以保证在写入 atomic 变量之后的所有内存访问都被完成,并且其他线程对 atomic 变量的读取必须在当前线程进行写入之前完成,从而确保线程之间的数据同步。
  5. memory_order_seq_cst
    该内存模型是最严格的一种内存模型,可以保证所有的操作都按照程序中指定的顺序执行。在该内存模型下,所有的读取和写入都是按照全局的顺序进行的,因此可以保证所有线程所看到的数据都是一致的。但是,由于要保证全局的顺序,因此会带来一定的性能损失。

需要注意的是,使用较弱的内存模型可以提高程序的性能,但是会增加代码的复杂度和调试难度。在实际应用中,需要根据具体的需求和场景选择适当的内存模型。

5.2.2 操作std::atomic_flag

std::atomic_flag是最简单的标准原子类型,表示一个布尔标志。该类型的对象两种状态:成立或置零。二者必居其一。std::atomic_flag类型的对象必须由宏ATOMIC_FLAG_INIT初始化,他把标志初始化为置零状态:std::atomic_flag f = ATOMIC_FLAG_INIT。无论在哪里声明,也无论处于什么作用域,std::atomic_flag对象永远以置零状态开始。

我们无法从std::atomic_flag对象拷贝构造出另一个对象,也无法向另一个对象拷贝赋值,这两个限制并非std::atomic_flag独有,所有原子类型都同样受限。原因是按定义,原子类型上的操作全都是原子化的,但拷贝赋值和拷贝构造都涉及两个对象,而牵涉两个不同对象的单一操作却无法原子化。在拷贝构造或拷贝赋值的过程中,必须先从来源对象读取值,再将其写出到目标对象。这是在两个独立对象上的两个独立操作,其组合是不可能原子化的。所以原子对象禁止拷贝赋值和拷贝构造。

std::atomic_flag 对象只有两个状态:已设置(set)和未设置(clear),它提供了以下两个操作:

  1. test_and_set():将标志设置为已设置,并返回之前的状态(即返回之前是否已设置)。
  2. clear():将标志设置为未设置。

这两个操作都是原子操作,可以保证在多线程环境下安全地操作同一标志。
下面是一个简单的示例,演示了如何使用std::atomic_flag来实现基本的互斥机制:

#include <atomic>
#include <thread>

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void foo()
{
    while (lock.test\_and\_set(std::memory_order_acquire))
    {
        // 等待锁被释放
    }
// 在这里执行临界区代码
    std::cout << "Hello, world!" << std::endl;
    lock.clear(std::memory_order_release);
}

int main()
{
    std::thread t1(foo);
    std::thread t2(foo);

    t1.join();
    t2.join();

    return 0;
}

在这个例子中,lock是一个std::atomic_flag对象,用来实现互斥。当一个线程需要访问受保护的资源时,它会调用test_and_set()方法来尝试获取锁,如果返回值为真,表示锁已经被其他线程获取,那么当前线程就会等待锁被释放;如果返回值为假,表示锁当前未被占用,那么当前线程就可以执行需要保护的代码。

当线程执行完需要保护的代码后,调用clear()方法来释放锁。

需要注意的是,在test_and_set()和clear()方法中,我们都使用了内存序参数std::memory_order_acquire和std::memory_order_release,这是为了确保对内存的访问是有序的。具体来说,std::memory_order_acquire确保在获取锁之前的所有读操作都在获取锁之前完成,而std::memory_order_release确保在释放锁之后的所有写操作都在释放锁之后完成。这样可以确保线程间的同步,避免出现不可预测的错误。

总之,std::atomic_flag可以用来实现简单的互斥机制,但是它并不适用于所有情况。如果需要更复杂的同步机制,可以考虑使用更高级的同步原语,如std::mutex。由于std::atomic_flag严格受限,甚至不支持单纯的无修改插值操作,无法用作普通的布尔标志,因此最好还是使用std::atomic 。下面我们来介绍一下std::atomic

5.2.3 操作std::atomic

std::atomic是一个模板类,它封装了一个布尔类型的原子变量,可以保证多线程环境下对它的操作是原子的,也就是说不会出现数据竞争或不一致的情况。它有以下几个成员函数:

  • 构造函数:可以用一个布尔值或另一个std::atomic对象来初始化原子变量。
  • operator=:可以用一个布尔值或另一个std::atomic对象来赋值给原子变量。
  • is_lock_free:可以检查原子变量是否是无锁的,也就是说是否不需要使用锁或其他同步机制来保证原子性。
  • store:可以用一个布尔值或另一个std::atomic对象来修改原子变量的值,并指定内存顺序。
  • load:可以读取原子变量的值,并指定内存顺序。 operator T:可以隐式地将原子变量转换为布尔类型,并返回其当前值。
  • exchange:可以用一个布尔值或另一个std::atomic对象来替换原子变量的值,并返回旧值,并指定内存顺序。
  • compare_exchange_weak和compare_exchange_strong:可以将原子变量与一个期望值进行比较,如果相等,就将其改为一个新值,并返回true;如果不相等,就将其当前值赋给期望值,并返回false。这两个函数的区别在于compare_exchange_weak可能会在没有必要时返回false,而compare_exchange_strong只有在确实不相等时才返回false。这两个函数都需要指定成功和失败时的内存顺序。

compare_exchange_weak和compare_exchange_strong函数都是用于原子地比较并交换std::atomic对象的值的12。它们的基本用法是:

bool compare\_exchange\_weak(T& expected, T desired,
                           std::memory_order success,
                           std::memory_order failure);

bool compare\_exchange\_strong(T& expected, T desired,
                             std::memory_order success,
                             std::memory_order failure); 

  1. 这两个函数都会将std::atomic对象的当前值与expected参数进行比较,如果相等,就将desired参数赋值给std::atomic对象,并返回true;如果不相等,就将当前值赋值给expected参数,并返回false。
  2. 它们的区别在于,compare_exchange_weak可能会在比较相等时失败(即产生伪失败),而compare_exchange_strong则不会.
    这是因为compare_exchange_weak只保证最终结果是正确的,而不保证每次操作都是正确的。这样可以提高性能,但也需要在循环中使用.
  3. compare_exchange_strong则保证每次操作都是正确的,但也可能更慢. 一般来说,当需要在循环中使用比较交换时,选择compare_exchange_weak会有更好的性能;当不需要循环时,选择compare_exchange_strong会更简单和安全.
  4. compare_exchange_weak可能会在比较相等时失败的原因是,它在某些平台上是用一系列的指令来实现的,而不是一个原子指令。这样可以提高性能,但也可能导致偶然的失败。这种失败并不会影响最终结果的正确性,只要我们在循环中重试就可以了。但是如果我们不需要循环,或者循环中有其他逻辑依赖于比较交换的结果,那么就应该使用compare_exchange_strong来避免伪失败。

std::atomic也支持一些基本的操作符重载,比如operator=()和operator bool()等,这些操作符可以像普通布尔类型一样使用。

下面通过一些实例进行理解:
一个使用compare_exchange_weak的例子是实现一个自旋锁。自旋锁是一种简单的同步原语,它用一个原子变量表示锁的状态,0表示未锁定,1表示已锁定。要获取锁,就要不断地尝试将变量从0改为1,直到成功为止。要释放锁,就要将变量改回0。代码如下:

#include <atomic>
#include <thread>
#include <iostream>

std::atomic<bool> lock(false); // 初始化为未锁定

void f(int n) {
    while (lock.exchange(true)) {} // 获取锁
    std::cout << "Output from thread " << n << '\n';
    lock.store(false); // 释放锁
}

int main() {
    std::thread t1(f, 1);
    std::thread t2(f, 2);
    t1.join();
    t2.join();
}

这里使用了exchange函数来原子地设置变量为true,并返回之前的值。如果之前的值是false,说明获取到了锁;如果是true,说明没有获取到锁,需要继续循环。这里也可以用compare_exchange_weak来实现同样的功能:

#include <atomic>
#include <thread>
#include <iostream>

std::atomic<bool> lock(false); // 初始化为未锁定

void f(int n) {
    bool expected = false;
    while (!lock.compare\_exchange\_weak(expected, true)) { // 获取锁
        expected = false; // 重置expected
    }
    std::cout << "Output from thread " << n << '\n';
    lock.store(false); // 释放锁
}

int main() {
    std::thread t1(f, 1);
    std::thread t2(f, 2);
    t1.join();
    t2.join();
}

这里使用了compare_exchange_weak来尝试将变量从false改为true,并返回比较结果。如果比较结果是true,说明获取到了锁;如果是false,说明没有获取到锁,并且expected被赋值为当前值(即true),需要重置expected并继续循环。这里使用compare_exchange_weak可能会产生伪失败,但不影响最终结果的正确性。

一个使用compare_exchange_strong的例子是实现一个单例模式。单例模式是一种设计模式,它保证一个类只有一个实例,并提供全局访问点。要实现一个线程安全的单例模式,就要用原子操作来保证只有一个线程能创建实例,并返回给其他线程。代码如下:

#include <atomic>
#include <memory>

class Widget {
public:
   static Widget\* getInstance() {
      Widget\* tmp = instance.load(); // 加载当前实例指针
      if (tmp == nullptr) { // 如果为空,则尝试创建新实例
         tmp = new Widget;
         if (instance.compare\_exchange\_strong(nullptr, tmp)) { // 将新实例赋值给原子指针,并检查是否成功
            return tmp; // 成功则返回新实例指针
         } else {
            delete tmp; // 失败则删除新实例,并返回已存在的实例指针
            return instance.load();
         }
      } else {
         return tmp; // 如果不为空,则直接返回当前实例指针
      }
   }

private:
   static std::atomic<Widget\*> instance; // 原子指针存储唯一实例

   Widget() {} // 私有构造函数防止外部创建

   Widget(const Widget&) = delete; 
   Widget& operator=(const Widget&) = delete;
};

std::atomic<Widget\*> Widget::instance(nullptr); 

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

mg-PBsg7va8-1715546847850)]
[外链图片转存中…(img-O0mvtZp4-1715546847851)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • 12
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值