当前位置: 首页 > news >正文

原子操作与锁实现

文章目录

  • 一、概述
    • 1.1 多线程与共享变量
    • 1.2 原子操作
    • 1.3 原子变量
    • 1.4 原子操作的实现与平台相关性
  • 二、原子操作常用接口
    • 2.1 `is_lock_free()`
    • 2.2 `store()`
    • 2.3 `load()`
    • 2.4 `exchange()`
    • 2.5 `compare_exchange_weak` 与 `compare_exchange_strong`
    • 2.6 `fetch_add`, `fetch_sub`, `fetch_or`, `fetch_xor` 等
  • 三、代码示例
    • 3.1 代码整体目的
    • 3.2 宏控制
    • 3.3 全局变量 `count`
    • 3.4 函数 `incrby(int num)`
    • 3.5 主函数执行流程
    • 3.6 并发问题验证

一、概述

1.1 多线程与共享变量

  1. 多线程环境

    • 多个线程同时运行,共享同一进程的资源(如内存、变量)。
    • 线程之间可能同时读写同一变量,导致数据不一致或错误。
  2. 共享变量的问题

    • 如果多个线程同时修改一个变量,结果可能不可预测。
    • 例如:两个线程同时对一个计数器进行+1操作,最终可能只增加了一次。
  3. 竞态条件(Race Condition)

    • 指程序的输出依赖于线程执行的顺序,结果不确定。
    • 解决方法:同步机制,如互斥锁、原子操作等。
机制优点缺点
互斥锁通用、可靠开销大、可能阻塞
原子操作轻量、高效、无阻塞仅适用于简单操作

1.2 原子操作

  1. 什么是原子操作?

    • 原子操作是指一个或多个步骤组成的操作,这些步骤要么全部执行,要么都不执行,不会被其他线程打断
    • 例如:对一个变量的“读-改-写”操作(如i++)如果不是原子的,就可能被中断。
  2. 为什么需要原子操作?

    • 多线程环境下,确保对共享变量的操作在执行时不会被干扰
    • 避免竞态条件,确保数据一致性。
    • 提高性能:比互斥锁更轻量,不需要上下文切换。
  3. 原子操作的例子

    • 简单的赋值、加法、交换等操作在某些平台上可以是原子的。
    • 但复杂的操作(如i++)通常不是原子的,需要特殊处理。

1.3 原子变量

  1. 什么是原子变量?

    • 一种特殊类型的变量,对其进行的操作是原子的。
    • 通常通过硬件支持或底层指令实现。
  2. 如何使用原子变量?

    • 在C++中可使用std::atomic<T>类型。
    • 在Java中可使用AtomicIntegerAtomicReference等。

1.4 原子操作的实现与平台相关性

  1. 底层实现方式

    • 依赖CPU指令集(如x86的LOCK前缀指令、ARM的LDREX/STREX指令)。
    • 编译器或运行时库会将这些指令封装成原子操作接口。
  2. 平台相关性

    • 不同CPU架构对原子操作的支持程度不同。
    • 编程语言(如C++、Java)提供了跨平台的原子操作抽象,但性能可能因平台而异。
  3. 注意事项

    • 不是所有操作都能原子化,复杂操作仍需锁机制。
    • 原子变量通常只能用于基本类型(如整型、指针)。

二、原子操作常用接口

2.1 is_lock_free()

功能:判断该原子对象是否支持无锁操作(lock-free)

  • 返回值:

    • true:支持无锁原子操作,操作完全由 CPU 原子指令实现,不需要互斥锁。
    • false:需要内部加锁(可能性能较低,但仍保证原子性)。
  • 使用场景:

    • 性能关键的场合,如果需要尽量减少锁开销。
  • 示例:

#include <atomic>
#include <iostream>std::atomic<int> counter;
int main() {if (counter.is_lock_free()) {std::cout << "counter supports lock-free atomic operations\n";} else {std::cout << "counter uses internal locking\n";}
}

注意:不同平台或不同类型(如较大结构体)可能返回不同结果。

2.2 store()

store(T desired, std::memory_order order)

功能:将一个值写入原子对象。

  • 参数:

    • desired:要存储的新值。
    • order:内存顺序,常用:
      • memory_order_relaxed:只保证原子性,不保证顺序。
      • memory_order_release:保证之前的写操作对其他线程可见。
      • memory_order_seq_cst:顺序一致性,最严格。
  • 示例:

std::atomic<int> counter{0};
counter.store(5, std::memory_order_relaxed); // 设置 counter = 5

注意:store 只修改原子对象的值,如何被其他线程看到,取决于内存顺序。

2.3 load()

load(std::memory_order order)

功能:读取原子对象的当前值。

  • 参数:

    • order:内存顺序,常用:
      • memory_order_relaxed:只保证原子性。
      • memory_order_acquire:确保之后的操作不会被提前到此读之前。
      • memory_order_seq_cst:全局顺序一致。
  • 示例:

std::atomic<int> counter{10};
int val = counter.load(std::memory_order_acquire);

注意:使用 acquireseq_cst 时,可以配合 store(..., release) 保证线程间同步。

2.4 exchange()

exchange(T desired, std::memory_order order)

功能:原子地用新值替换当前值,并返回替换前的旧值。

  • 原子性:操作不可分割,读取和写入在其他线程看来是同时完成的。
  • 示例:
std::atomic<int> counter{10};
int old = counter.exchange(20, std::memory_order_seq_cst);
std::cout << "old value: " << old << ", new value: " << counter.load() << "\n";
  • 适用场景:

    • 原子交换值,例如实现锁、标志位或状态切换。
  • 注意:

    • 返回值是修改前的旧值。
    • store 不同,exchange 保证了原子读取+写入。

2.5 compare_exchange_weakcompare_exchange_strong

功能:原子比较并交换(CAS, Compare-And-Swap)

  • 共同参数

    • T& expected:期望值,如果当前值等于 expected,就替换;否则把当前值写回 expected
    • T val:新的值。
    • memory_order success:成功替换时的内存顺序。
    • memory_order failure:替换失败时的内存顺序(通常 <= success)。
  • 返回值

    • true:替换成功。
    • false:替换失败。
  • 区别

    • weak:允许在硬件上因“伪失败”而失败,需要循环重试,性能更好。
    • strong:保证只在值不等时失败,更稳定但可能略慢。
  • 示例:

std::atomic<int> counter{10};
int expected = 10;
bool success = counter.compare_exchange_weak(expected, 20, std::memory_order_seq_cst, std::memory_order_relaxed);
if(success) {std::cout << "CAS succeeded, counter = " << counter.load() << "\n";
} else {std::cout << "CAS failed, expected now = " << expected << "\n";
}

注意

  • weak 在循环中常用:
int expected = counter.load();
while (!counter.compare_exchange_weak(expected, expected+1)) {// expected 被更新为当前值,循环重试
}
  • CAS 是实现很多无锁数据结构的基础。

2.6 fetch_add, fetch_sub, fetch_or, fetch_xor

功能:原子地对当前值做运算,并返回修改前的值。

  • fetch_add(x):加上 x。
  • fetch_sub(x):减去 x。
  • fetch_or(x):按位或。
  • fetch_xor(x):按位异或。
  • 内部保证原子性,可同时读和写。
  • 示例:
std::atomic<int> counter{5};
int old = counter.fetch_add(3); // counter += 3, 返回旧值
std::cout << "old: " << old << ", new: " << counter.load() << "\n";

应用场景

  • 原子计数器
  • 位操作标志
  • 无锁队列或栈的索引更新

注意

  • 同样可以指定内存顺序。
  • 返回值是修改前的值。

三、代码示例

通过多线程对一个共享变量 count 进行累加,分别使用 普通变量 / 互斥锁 / 自旋锁 / 原子操作 来保证线程安全,从而对比不同并发控制手段的实现方式。

#include <chrono>
#include <iostream>
#include <thread>
#include <assert.h>// #define USE_ATOMIC 0
#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};
#elseint count = 0;
#endifvoid incrby(int num) {for (int i=0; i < num; i++) {
#if USE_MUTEXmtx.lock();++count;mtx.unlock();
#elif USE_SPINLOCKspinlock_lock(&spin);++count;spinlock_unlock(&spin);
#elif USE_ATOMIC// std::this_thread::sleep_for(std::chrono::milliseconds(1));count.fetch_add(1);
#else++count;
#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;
}

3.1 代码整体目的

  • 多线程环境下,对共享变量 count 进行安全的自增操作。
  • 通过不同的同步机制(互斥锁、自旋锁、原子变量)确保 count 的最终结果正确(应该为 2000)。
  • 验证并发环境下数据竞争问题以及常见解决方式。

3.2 宏控制

代码中使用了宏定义来切换不同的实现方式:

  • #define USE_ATOMIC 1

    • 使用 原子操作(std::atomic),依赖 CPU 的原子指令,无需显式加锁。
  • #if USE_MUTEX

    • 使用 互斥锁(std::mutex),线程进入关键区时必须加锁,离开时释放。
  • #elif USE_SPINLOCK

    • 使用 自旋锁(spinlock),线程忙等待直到获得锁。
  • #else

    • 不加锁、不原子化,会产生数据竞争,结果不确定。

通过修改宏,可以方便地对比不同同步机制的效果与性能。

3.3 全局变量 count

  • 普通实现int count = 0;

    • 不安全,多线程会产生竞态条件(data race)。
  • 互斥锁 / 自旋锁:依旧是普通 int,但通过锁保证访问安全。

  • 原子实现std::atomic<int> count{0};

    • 保证自增操作是原子性的,无需额外锁。

3.4 函数 incrby(int num)

作用:让某个线程执行 num++count,并保证线程安全。

不同实现:

  • 互斥锁

    mtx.lock();
    ++count;
    mtx.unlock();
    
    • 使用 RAII 会更安全(std::lock_guard),此处是手动 lock/unlock。
  • 自旋锁

    spinlock_lock(&spin);
    ++count;
    spinlock_unlock(&spin);
    
    • 自旋等待,适合临界区很短的场景,否则 CPU 消耗大。
  • 原子操作

    count.fetch_add(1);
    
    • 底层依赖硬件原子指令(如 x86 上的 LOCK XADD),无锁化实现。
  • 无保护

    ++count;
    
    • 会有竞态条件,最终结果可能小于期望值。

3.5 主函数执行流程

  • 初始化:若使用自旋锁,需要 spinlock_init(&spin);
  • 执行循环 100 次,每次做一次完整测试:
    1. count 归零(若是原子变量,用 count.store(0))。
    2. 启动 4 个线程,每个线程执行 incrby(500)
    3. 主线程等待所有线程结束(join())。
    4. 检查 count 是否为 2000
      • 期望值:4 * 500 = 2000
      • 如果不为 2000,打印出错信息并中断。

3.6 并发问题验证

  1. 不加锁、不原子化:出现 count < 2000,因为多个线程可能同时读取和写入同一个内存地址。

    • 自增 ++count 不是原子操作
    • 编译后大致会变成三步:
      1. 从内存读取 count 到 CPU 寄存器
      2. 在寄存器里执行 +1
      3. 把结果写回 count 的内存地址
    • 出错举例:
      假设 `count = 100`,两个线程同时执行 `++count`:线程 A:读 count = 100计算 100 + 1 = 101线程 B:也读 count = 100计算 100 + 1 = 101线程 A:写回 count = 101
      线程 B:也写回 count = 101
      
    • 结果:两次自增,最终只增加了 1。
  2. 加锁/原子操作

    • 结果稳定等于 2000,没有数据丢失。
http://www.dtcms.com/a/358416.html

相关文章:

  • 由于不对称GND过孔配置,差分信号过孔上的差模到共模转换
  • SQL相关知识 CTF SQL注入做题方法总结
  • seafile-setup-troubleshooting_# Seafile 安装与问题解决记录 # Seafile/Seahub 启动问题记录文档
  • Scikit-learn Python机器学习 - Scikit-learn加载数据集
  • C/C++:AddressSanitizer内存检测工具
  • 《以奋斗者为本》读书笔记(上篇:价值管理)
  • Ethan开发者创新项目日报 | 2025-08-30
  • MySQL之事务
  • 渲染数据列表:`map` 方法与 `key` 的奥秘
  • Rust 泛型:抽象与性能的完美融合(零成本抽象的终极指南)
  • sql简单练习——随笔记
  • Deepseek法务提示指令收集
  • 【前端教程】MIUI 官网界面设计与实现全解析
  • ceph配置集群
  • 详情Redis的Zset结构
  • STM32 之BMP280的应用--基于RTOS的环境
  • React学习教程,从入门到精通, ReactJS - 优点与缺点(5)
  • 学习stm32 窗口看门狗
  • 鸿蒙ArkUI 基础篇-12-List/ListItem-界面布局案例歌曲列表
  • Shell脚本命令扩展
  • 回归问题的损失函数
  • 06.《STP 基础原理与配置详解》
  • 学习python第14天
  • Spark mapGroups 函数详解与多种用法示例
  • 神经网络正则化三重奏:Weight Decay, Dropout, 和LayerNorm
  • 嵌入式硬件电路分析---AD采集电路
  • pyqt5的简单开发可视化界面的例子
  • 【重学 MySQL】九十三、MySQL的字符集的修改与底层原理详解
  • Linux学习----归档和传输文件实用指南
  • java报错问题解析