原子锁操作

一、原子操作与锁

1.1、什么是原子操作?

在多线程环境下,确保对共享变量的操作在执行时不会被干扰,从而避免竞态条件,即在同一时刻只允许一个线程可以执行该操作。

1.2、原子性的底层实现

  1. 从存储体系结构来看

首先是寄存器,离CPU的位置最近,CPU可以直接访问寄存器,寄存器的速度最快。然后是高速缓存(L1、L2、L3),再到内存;速度是依次下降的

其中L1和L2缓存是每个核独占的,L3是多个核共享的。

之所以要设计这样的存储体系结构,主要是为了解决CPU和内存之间的速度不匹配问题。

以自己电脑为例,可以清晰看到三级缓存的存在,以及它们各自的容量.

  1. 从CPU处理器来看
  • 何为原子性?
    原子性指的是一个操作是不可中断的,要么全部执行成功,要么就都不执行。(数据库事务也是如此,要么全部成功,要么就都不执行。)
  • 单处理器单核(单线程):
    • 保证操作指令不被打断。
      • 通过自旋锁
      • 屏蔽中断
  • 多处理器多核(多线程):
    • 除了通过自旋锁和屏蔽中断保证原子性外,还可以通过避免其他核心操作相关的内存空间。
      • 以往数据发生改变通过总线广播,那么可以通过锁总线的方式,避免其他核心访问内存空间。(低效)
      • 只锁定相关内存空间,而不是整个总线。比如锁住变量i,不影响变量j的操作
  1. CPU缓存的读写策略
  • 读操作:
  1. CPU发出主存地址后,首先判断该地址是否在缓存中
  2. 如果在,则直接从缓存中读取数据
  3. 如果不在,一方面从主存中读取数据,另一方面将该地址所在的缓存行加载到CPU的缓存中
  4. 如果CPU缓存中没有空闲的缓存行,则会采用替换算法(如FIFO,LRU,随机替换)将旧的缓存行替换掉,并将新读取的数据加载到新的缓存行中
  • 写操作:
    对于写操作就比较复杂,因为对Cahce块内写入的信息,必须与被映射的主存块内的信息完全一致。当程序运行过程中需对某个单元进行写操作时,会出现如何使Cache与主存内容保持一致 的问题。目前有以下几种方法:

    1. 写直达法(Write-Through):
      写操作时,既写入Cache,也写入主存。能保证主存和Cache的数据始终保持一致,但增加了访存次数。
    2. 写回法(Write-Back):
    • 写操作时,只写入Cache,而不写入主存,当Cache中的数据被替换时,再将该数据写回主存。减少了访存次数,但增加了数据不一致的风险。
    • 为了识别Cache中的数据是否与主存一致,Cache块中通常会有一个标志位(脏位),该位有两个状态:"清"(表示为被修改过,与主存一致)和"浊"(表示修改过,与主存不一致)
    • 在Cache替换时,"清"的Cache不必写回主存
    • 在写Cache时,要将标志位设置为"浊",替换时此Cache块必须写回主存,同时标志位置为"清"。
  1. 缓存一致性协议
  • 为什么会出现缓存不一致问题?
    • 现代CPU基本都是多核的
    • 现代CPU基本都采用写回策略
  • 解决方案--缓存一致性协议(MESI )
    • MESI: 通过总线嗅探策略(将读写请求通过总线广播给所有核心,核心根据本地状态进行响应)实现事务串行化,通过状态机降低总线带宽压力。
    • 状态:
      • M(Modified):数据已经被修改 ,但未同步到内存中。如果其他核心要读数据,需要将数据从缓存同步到内存中,并将状态转为S(Shared)
      • E(Exclusive): 独占,某个数据只在某核心中,此时缓存和内存中的数据一致
      • S(Shared): 共享,多个核心可以访问该数据,此时缓存和内存中的数据一致
      • I(Invalidated): 无效,某数据在该核心中失效,不是最新数据。
    • 事件:
      • PrRd: 核心请求从缓存中读取数据
      • PrWr: 核心请求向缓存中写入数据
      • BusRd: 总线嗅探器收到来自其他核心的读出缓存请求
      • BusRdX: 总线嗅探器收到另一核心写一个其不拥有的缓存块的请求
      • BusUpgr: 总线嗅探器收到另一核心写一个其拥有的缓存块的请求
      • Flush: 总线嗅探器收到另一核心把一个缓存块写回到主存的请求
      • FlushOpt: 总线嗅探器收到一个缓存块被放置在总线,以提供给另一个核心的请求,类似Flush,但不写回主存,只作用与缓存

***小结:***原子性操作的底层实现 = 硬件自旋锁 + 屏蔽中断 + 锁总线锁住 ME状态; 锁总线锁住ME状态,是根据现代CPU的写回策略以及保持缓存一致性来决定的。

1.3、内存序

  • 什么是内存序:
    • CPU和编译器,会对代码进行优化,比如指令重排。

      c++ 复制代码
      // 原来代码执行顺序:
      int i = 1;
      int j = 1;
      i += 1;
      j += 2;
      
      // 经过优化后的执行顺序可能为:
      int i = 1;
      i += 1;
      
      int j = 1;
      j += 2;
      // 将相关的变量放在一起,减少缓存失效次数

      这样处理在单线程中没有问题,但是在多线程中就会出现问题。时序发生改变,导致结果不可预期。

      所以内存序是为了指导CPU和编译器进行优化时,需要遵守的规则 以及操作某个核心中的数据时,是否需要同步至其他核心

  • 内存序规定了什么:
    • 规定了多个线程访问同一个内存地址的语义
    • 某个线程对内存地址的更新何时能被其他线程看见
    • 某个线程对内存地址访问附近可以做怎么样的优化
  • 内存序具体规则:
    • memory_order_relaxed:松散内存序,只用来保证对原子对象的操作具备原子性,不保证多线程的执行顺序(随便优化)

      • 比如指令序:写入a,写入b,写入c,写入x,写入a,读出b,读出c对写入x进行memory_order_relaxed
      • 可以得到:写入b,写入a,读出b,写入x,写入c,读出c,写入a,只保证写入x的顺序,其他顺序可以任意调整
    • memory_order_release: 释放操作,写操作,在写缓存的同时,会将数据同步到主存中,其他核心可以看见该核心对内存地址的更新

      • 只允许该指令后的指令能被优化到该指令之前,不允许该指令之前的指令被优化到该指令之后

    • memory_order_acquire: 获取操作,读操作,为保证数据一致性,会读取主存的数据,然后同步至缓存当中,其他核心可以看见该核心对内存地址的更新

      • 只允许该指令之前的指令能被优化到该指令之后,不允许该指令之后的指令被优化到该指令之前
      • memory_order_release配合使用,保证内存操作的顺序

    • memory_order_consume: 不建议使用,在大多数平台上,只会影响编译器的优化

    • memory_order_acq_rel: 获取-释放操作 ,在memory_order_acquirememory_order_release的基础上,既保证该指令之前的指令不能被优化到该指令之后,又保证该指令之后的指令不能被优化到该指令之前,相当于存在内存屏障

    • memory_order_seq_cst: 顺序一致操作 ,对于读操作相当于memory_order_acquire,对于写操作相当于memory_order_release,对于读写操作相当于memory_order_acq_rel,是所有原子操作的默认内存序 ,并且会对所有使用该模型的原子操作建立一个全局顺序,保证了多个原子变量的操作在所有线程里观察到的操作顺序相同,但也是最慢的。

二、原子操作实践

2.1、游戏在线人数统计

以游戏在线人数统计为背景 ,统计某个时间点在线人数。

  • 不使用原子操作之前
cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>

// 全局变量 - 在线人数
int online_players = 0;

// 模拟玩家登录
void player_login() {
    for (int i = 0; i < 10000; ++i) {
        int current = online_players;  // 1. 读取
        current = current + 1;         // 2. 修改
        online_players = current;      // 3. 写入
    }
}

// 模拟玩家登出
void player_logout() {
    for (int i = 0; i < 5000; ++i) {
        int current = online_players;
        current = current - 1;
        online_players = current;
    }
}

void test_non_atomic() {
    std::cout << "=== 非原子操作测试 ===" << std::endl;

    // 记录开始值
    int start_value = online_players;

    // 创建多个线程模拟玩家登录
    std::vector<std::thread> login_threads;
    for (int i = 0; i < 10; ++i) {
        login_threads.emplace_back(player_login);
    }

    // 创建多个线程模拟玩家登出
    std::vector<std::thread> logout_threads;
    for (int i = 0; i < 5; ++i) {
        logout_threads.emplace_back(player_logout);
    }

    // 等待所有线程完成
    for (auto& t : login_threads) t.join();
    for (auto& t : logout_threads) t.join();

    // 计算预期结果
    int expected = start_value + (10 * 10000) - (5 * 5000);
    std::cout << "初始在线人数: " << start_value << std::endl;
    std::cout << "预期在线人数: " << expected << std::endl;
    std::cout << "实际在线人数: " << online_players << std::endl;
    std::cout << "是否正确: " << (online_players == expected ? "是" : "否") << std::endl;
    std::cout << "错误差值: " << (expected - online_players) << std::endl;
}

可以明显看到,实际统计的人数和预期的人数完全不一致,而且差距还挺大。

  • 使用原子操作之后
cpp 复制代码
std::atomic<int> atomic_online_players(0);

// 使用原子操作-默认(顺序一致性)
void player_login_atomic_seq_cst()
{
    for (int i = 0; i < 10000; ++i) {
        atomic_online_players.fetch_add(1, std::memory_order_seq_cst);
    }
}

void player_logout_atomic_seq_cst() {
    for (int i = 0; i < 5000; ++i) {
        atomic_online_players.fetch_sub(1, std::memory_order_seq_cst);
    }
}

void test_atomic_basic() {
    std::cout << "\n=== 原子操作默认版本(顺序一致性) ===" << std::endl;

    atomic_online_players = 0;  // 重置计数器
    int start_value = atomic_online_players.load();

    // 创建线程
    std::vector<std::thread> login_threads;
    std::vector<std::thread> logout_threads;

    for (int i = 0; i < 10; ++i) {
        login_threads.emplace_back(player_login_atomic_seq_cst);
    }

    for (int i = 0; i < 5; ++i) {
        logout_threads.emplace_back(player_logout_atomic_seq_cst);
    }

    for (auto& t : login_threads) t.join();
    for (auto& t : logout_threads) t.join();

    auto end = std::chrono::high_resolution_clock::now();

    int expected = start_value + (10 * 10000) - (5 * 5000);
    std::cout << "初始在线人数: " << start_value << std::endl;
    std::cout << "预期在线人数: " << expected << std::endl;
    std::cout << "实际在线人数: " << atomic_online_players.load() << std::endl;
    std::cout << "是否正确: " << (atomic_online_players == expected ? "是" : "否") << std::endl;
}

2.2、原子操作-六种内存序对比

  • 松散内存序(memory_order_relaxed)
cpp 复制代码
// 松散内存序 - 只保证原子性,不保证顺序
void player_login_atomic_relaxed()
{
    for (int i = 0; i < 10000; ++i) {
        atomic_online_players.fetch_add(1, std::memory_order_relaxed);
    }
}

void player_logout_atomic_relaxed()
{
    for (int i = 0; i < 5000; ++i) {
        atomic_online_players.fetch_sub(1, std::memory_order_relaxed);
    }
}

void test_relaxed() 
{
    std::cout << "\n=== memory_order_relaxed ===" << std::endl;
    std::cout << "只保证原子性,不保证内存顺序" << std::endl;

    atomic_online_players = 0;  // 重置计数器
    int start_value = atomic_online_players.load();

    auto start = std::chrono::high_resolution_clock::now();

    // 创建线程
    std::vector<std::thread> login_threads;
    std::vector<std::thread> logout_threads;

    for (int i = 0; i < 10; ++i) {
        login_threads.emplace_back(player_login_atomic_relaxed);
    }

    for (int i = 0; i < 5; ++i) {
        logout_threads.emplace_back(player_logout_atomic_relaxed);
    }

    for (auto& t : login_threads) t.join();
    for (auto& t : logout_threads) t.join();

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    int expected = start_value + (10 * 10000) - (5 * 5000);
    std::cout << "初始在线人数: " << start_value << std::endl;
    std::cout << "预期在线人数: " << expected << std::endl;
    std::cout << "实际在线人数: " << atomic_online_players.load() << std::endl;
    std::cout << "是否正确: " << (atomic_online_players == expected ? "是" : "否") << std::endl;
    std::cout << "执行时间: " << duration.count() << " 微秒" << std::endl;
}
  • 释放内存序(release)
cpp 复制代码
void player_login_atomic_release()
{
    // 单独使用release内存序是错误操作
    for (int i = 0; i < 10000; ++i) {
        atomic_online_players.fetch_add(1, std::memory_order_release);
    }
}

void player_logout_atomic_release()
{
    for (int i = 0; i < 5000; ++i) {
        atomic_online_players.fetch_sub(1, std::memory_order_release);
    }
}

void test_release()
{
    std::cout << "\n=== memory_order_release ===" << std::endl;
    std::cout << "释放内存序\n";

    atomic_online_players = 0;  // 重置计数器
    int start_value = atomic_online_players.load();

    auto start = std::chrono::high_resolution_clock::now();

    // 创建线程
    std::vector<std::thread> login_threads;
    std::vector<std::thread> logout_threads;

    for (int i = 0; i < 10; ++i) {
        login_threads.emplace_back(player_login_atomic_release);
    }

    for (int i = 0; i < 5; ++i) {
        logout_threads.emplace_back(player_logout_atomic_release);
    }

    for (auto& t : login_threads) t.join();
    for (auto& t : logout_threads) t.join();

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    int expected = start_value + (10 * 10000) - (5 * 5000);
    std::cout << "初始在线人数: " << start_value << std::endl;
    std::cout << "预期在线人数: " << expected << std::endl;
    std::cout << "实际在线人数: " << atomic_online_players.load() << std::endl;
    std::cout << "是否正确: " << (atomic_online_players == expected ? "是" : "否") << std::endl;
    std::cout << "执行时间: " << duration.count() << " 微秒" << std::endl;
}
  • 获取内存序(acquire)
cpp 复制代码
void player_login_atomic_acquire()
{
    // 单独使用acquire内存序是错误操作
    for (int i = 0; i < 10000; ++i) {
        atomic_online_players.fetch_add(1, std::memory_order_acquire);
    }
}

void player_logout_atomic_acquire()
{
    for (int i = 0; i < 5000; ++i) {
        atomic_online_players.fetch_sub(1, std::memory_order_acquire);
    }
}

void test_acquire()
{
    std::cout << "\n=== memory_order_acquire ===" << std::endl;
    std::cout << "获取内存序\n";

    atomic_online_players = 0;  // 重置计数器
    int start_value = atomic_online_players.load();

    auto start = std::chrono::high_resolution_clock::now();

    // 创建线程
    std::vector<std::thread> login_threads;
    std::vector<std::thread> logout_threads;

    for (int i = 0; i < 10; ++i) {
        login_threads.emplace_back(player_login_atomic_acquire);
    }

    for (int i = 0; i < 5; ++i) {
        logout_threads.emplace_back(player_logout_atomic_acquire);
    }

    for (auto& t : login_threads) t.join();
    for (auto& t : logout_threads) t.join();

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    int expected = start_value + (10 * 10000) - (5 * 5000);
    std::cout << "初始在线人数: " << start_value << std::endl;
    std::cout << "预期在线人数: " << expected << std::endl;
    std::cout << "实际在线人数: " << atomic_online_players.load() << std::endl;
    std::cout << "是否正确: " << (atomic_online_players == expected ? "是" : "否") << std::endl;
    std::cout << "执行时间: " << duration.count() << " 微秒" << std::endl;
}
  • 获取释放内存序(acq_rel)
cpp 复制代码
void player_login_atomic_acq_rel()
{
    for (int i = 0; i < 10000; ++i) {
        atomic_online_players.fetch_add(1, std::memory_order_acq_rel);
    }
}

void player_logout_atomic_acq_rel()
{
    for (int i = 0; i < 5000; ++i) {
        atomic_online_players.fetch_sub(1, std::memory_order_acq_rel);
    }
}

void test_acq_rel()
{
    std::cout << "\n=== memory_order_acq_rel ===" << std::endl;
    std::cout << "获取-释放内存序\n";

    atomic_online_players = 0;  // 重置计数器
    int start_value = atomic_online_players.load();

    auto start = std::chrono::high_resolution_clock::now();

    // 创建线程
    std::vector<std::thread> login_threads;
    std::vector<std::thread> logout_threads;

    for (int i = 0; i < 10; ++i) {
        login_threads.emplace_back(player_login_atomic_acq_rel);
    }

    for (int i = 0; i < 5; ++i) {
        logout_threads.emplace_back(player_logout_atomic_acq_rel);
    }

    for (auto& t : login_threads) t.join();
    for (auto& t : logout_threads) t.join();

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    int expected = start_value + (10 * 10000) - (5 * 5000);
    std::cout << "初始在线人数: " << start_value << std::endl;
    std::cout << "预期在线人数: " << expected << std::endl;
    std::cout << "实际在线人数: " << atomic_online_players.load() << std::endl;
    std::cout << "是否正确: " << (atomic_online_players == expected ? "是" : "否") << std::endl;
    std::cout << "执行时间: " << duration.count() << " 微秒" << std::endl;
}
  • 消费内存序(consume)
cpp 复制代码
// 消费内存序-不推荐使用,现在大部分平台将其实现为acquire
void player_login_atomic_consume()
{
    for (int i = 0; i < 10000; ++i) {
        atomic_online_players.fetch_add(1, std::memory_order_consume);
    }
}

void player_logout_atomic_consume()
{
    for (int i = 0; i < 5000; ++i) {
        atomic_online_players.fetch_sub(1, std::memory_order_consume);
    }
}
void test_consume()
{
    std::cout << "\n=== memory_order_consume ===" << std::endl;
    std::cout << "消费内存序-不推荐使用,现在大部分平台将其实现为acquire\n";

    atomic_online_players = 0;  // 重置计数器
    int start_value = atomic_online_players.load();

    auto start = std::chrono::high_resolution_clock::now();

    // 创建线程
    std::vector<std::thread> login_threads;
    std::vector<std::thread> logout_threads;

    for (int i = 0; i < 10; ++i) {
        login_threads.emplace_back(player_login_atomic_consume);
    }

    for (int i = 0; i < 5; ++i) {
        logout_threads.emplace_back(player_logout_atomic_consume);
    }

    for (auto& t : login_threads) t.join();
    for (auto& t : logout_threads) t.join();

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    int expected = start_value + (10 * 10000) - (5 * 5000);
    std::cout << "初始在线人数: " << start_value << std::endl;
    std::cout << "预期在线人数: " << expected << std::endl;
    std::cout << "实际在线人数: " << atomic_online_players.load() << std::endl;
    std::cout << "是否正确: " << (atomic_online_players == expected ? "是" : "否") << std::endl;
    std::cout << "执行时间: " << duration.count() << " 微秒" << std::endl;
}

int main()
{
    //test_non_atomic();
    test_atomic_basic();
    test_relaxed();
    test_release();
    test_acquire();
    test_acq_rel();
    test_consume();
    return 0;
}

小结:

  1. 可以明显看到,memory_order_relaxed的执行时间最短,因为它不保证任何内存序,只保证原子性;而memory_order_seq_cst最慢,因为它需要保证全局顺序一致性,建立单一全局操作顺序,需要最强的内存屏障。
  2. 不能单独使用``memory_order_acquirememory_order_release`,但为什么得到正确的结果?
    1. 因为目前只涉及一个原子变量操作
    2. 无依赖关系,不需要同步其他非原子变量的状态
相关推荐
moxiaoran57532 小时前
Spring AOP开发的使用场景
java·后端·spring
旖旎夜光6 小时前
C++(17)
c++·学习
一线大码6 小时前
Gradle 基础篇之基础知识的介绍和使用
后端·gradle
Java猿_6 小时前
Spring Boot 集成 Sa-Token 实现登录认证与 RBAC 权限控制(实战)
android·spring boot·后端
小王师傅666 小时前
【轻松入门SpringBoot】actuator健康检查(上)
java·spring boot·后端
Larry_Yanan7 小时前
Qt多进程(三)QLocalSocket
开发语言·c++·qt·ui
superman超哥7 小时前
仓颉语言中元组的使用:深度剖析与工程实践
c语言·开发语言·c++·python·仓颉
码事漫谈7 小时前
C++高并发编程核心技能解析
后端
码事漫谈7 小时前
C++与浏览器交织-从Chrome插件到WebAssembly,开启性能之门
后端