【C++】一篇文章悲观锁与乐观锁与其思想在C++语言中的应用

悲观锁(Pessimistic Lock)

很悲观,认为每次拿数据都会被别人修改,所以拿的时候就上锁。

  • 假设并发冲突一定会发生
  • 操作前先加锁,其他人必须等待
  • 阻塞式、排他锁

典型实现

  • 数据库:select ... for update
  • Java:synchronizedReentrantLock

优点

  • 强一致性,冲突多的时候更稳定
  • 写多读少场景效率高

缺点

  • 锁竞争、上下文切换开销大
  • 冲突少的时候浪费性能

乐观锁(Optimistic Lock)

核心思想:很乐观,认为一般不会冲突,只在更新时检查是否被改。

  • 假设冲突很少发生
  • 读不加锁,更新时才校验
  • 是非阻塞、无锁设计

典型实现

  1. 版本号机制(最常用)

    • 加字段 version

    • 更新时:

      sql 复制代码
      update table
      set ..., version = version + 1
      where id = ? and version = ?
    • 影响行数=0 说明冲突,重试/报错

  2. CAS(Compare And Swap)思想

    • 比较旧值 → 相等才更新
    • C++ : std::atomic原子类
    • Java:AtomicInteger 原子类

优点

  • 无锁,高并发、读多写少极快
  • 无死锁风险

缺点

  • 冲突多会导致大量重试,CPU 飙升
  • 只能保证单个变量原子性(ABA 问题)

简洁总结

  • 悲观锁:先上锁,再操作 → 安全但慢
  • 乐观锁:不加锁,更新时校验 → 快但要处理冲突

使用场景

  • 读多写少、高并发 → 乐观锁
  • 写多读少、要求强一致 → 悲观锁

在C++后端开发中,乐观锁和悲观锁的使用场景非常明确,核心是根据并发冲突概率业务一致性要求来选择。下面结合C++的具体实现和后端开发的典型场景,给你讲清楚怎么用、用在哪。

悲观锁在C++中的应用场景

悲观锁在C++中主要依赖互斥锁、自旋锁 等同步原语实现,核心用于写操作频繁、冲突概率高、强一致性要求的场景。

1. 实现(C++标准库std::mutex)

cpp 复制代码
#include <mutex>
#include <thread>
#include <iostream>

// 全局互斥锁(悲观锁核心)
std::mutex mtx;
int shared_data = 0;

// 写操作函数(多线程调用)
void write_data(int value) {
    // 加锁(悲观:认为一定会有竞争,先锁再操作)
    std::lock_guard<std::mutex> lock(mtx);
    shared_data = value;
    std::cout << "写入数据:" << shared_data << std::endl;
}

int main() {
    std::thread t1(write_data, 10);
    std::thread t2(write_data, 20);
    t1.join();
    t2.join();
    return 0;
}

2. 使用场景

(1)多线程操作共享资源(写多读少)
  • 场景:网关服务中多线程修改连接池、配置中心多线程更新配置、订单系统多线程修改订单状态。
  • 原因:写操作频繁,冲突概率高,悲观锁能保证绝对的线程安全,避免数据错乱。
  • 扩展:除了std::mutex,还会用std::recursive_mutex(可重入锁)、pthread_mutex_t(POSIX锁)。
(2)数据库层面的排他锁
  • 场景:金融系统扣减余额、秒杀系统扣减库存(C++程序调用SQL)。

  • 实现:C++程序执行select ... for update语句,锁定数据库行,避免并发更新导致数据不一致。

    cpp 复制代码
    // 伪代码:C++调用MySQL执行悲观锁SQL
    std::string sql = "SELECT balance FROM user WHERE id = 1 FOR UPDATE";
    mysql_query(conn, sql.c_str());
    // 扣减余额(此时该行已被锁定,其他线程/进程无法修改)
    sql = "UPDATE user SET balance = balance - 100 WHERE id = 1";
    mysql_query(conn, sql.c_str());
(3)临界区操作(必须原子执行)
  • 场景:日志系统多线程写入同一个日志文件、网络服务多线程修改全局连接计数。
  • 原因:这类操作不能被打断,悲观锁能保证临界区代码完整执行。

乐观锁在C++中的应用场景

乐观锁在C++中主要依赖CAS(Compare-And-Swap) 实现(C++11起提供原子操作库),核心用于读多写少、冲突概率低、追求高性能的场景。

典型实现(C++原子类/CAS)

cpp 复制代码
#include <atomic>
#include <thread>
#include <iostream>

// 原子变量(底层基于CAS,乐观锁核心)
std::atomic<int> shared_data = 0;

// 更新数据函数(多线程调用)
void update_data(int value) {
    int expected = shared_data.load(); // 读取当前值
    // CAS操作(乐观:先读值,更新时检查是否被修改,没修改才更新)
    while (!shared_data.compare_exchange_weak(expected, value)) {
        // 冲突时重试(expected会自动更新为最新值)
        std::cout << "数据已被修改,重试更新..." << std::endl;
    }
    std::cout << "成功更新数据:" << shared_data << std::endl;
}

int main() {
    std::thread t1(update_data, 10);
    std::thread t2(update_data, 20);
    t1.join();
    t2.join();
    return 0;
}

2. 使用场景

(1)高并发计数器(读多写少)
  • 场景:接口访问量统计、缓存命中率统计、秒杀系统的参与人数计数。
  • 原因:读操作远多于写操作,冲突概率低,CAS比互斥锁性能高一个量级(无锁开销)。
  • 扩展:C++11的std::atomic系列(atomic_intatomic_long)都是乐观锁实现,后端中常用于高性能统计。
(2)数据库版本号控制(C++程序配合DB)
  • 场景:电商系统更新商品库存(读多写少,大部分是查询库存,少量扣减)、用户资料修改(大部分是查看,少量编辑)。

  • 实现:C++程序通过版本号实现乐观锁,避免数据库锁竞争:

    cpp 复制代码
    // 伪代码:C++调用MySQL实现乐观锁
    // 1. 查询商品信息(含版本号)
    std::string sql = "SELECT stock, version FROM goods WHERE id = 1";
    mysql_query(conn, sql.c_str());
    int stock = ...; // 读取库存
    int version = ...; // 读取版本号
    
    // 2. 扣减库存(更新时校验版本号)
    sql = "UPDATE goods SET stock = stock - 1, version = version + 1 "
          "WHERE id = 1 AND version = " + std::to_string(version);
    int affected_rows = mysql_affected_rows(conn);
    if (affected_rows == 0) {
        // 版本号不匹配,说明有并发更新,重试或返回错误
        std::cout << "库存更新冲突,请重试" << std::endl;
    }
(3)无锁数据结构(高性能组件)
  • 场景:网关服务的无锁队列、缓存系统的无锁哈希表、高并发RPC框架的无锁连接池。
  • 原因:后端中间件对性能要求极高,无锁数据结构(基于乐观锁CAS)能避免锁竞争,提升吞吐量。
  • 示例:C++实现无锁栈(核心是CAS操作节点指针)。

乐观锁与悲观锁的选择准则

维度 悲观锁 乐观锁
适用场景 写多读少、冲突率高、强一致性 读多写少、冲突率低、高性能要求
C++实现 std::mutex、pthread_mutex std::atomic(CAS)、版本号
性能开销 锁竞争、上下文切换开销大 无锁开销,冲突时重试开销
典型应用 订单修改、余额扣减、日志写入 计数器、库存查询更新、无锁队列
相关推荐
国产化创客1 小时前
OpenClaw在树莓派全流程安装部署
linux·人工智能·github·agi
whycthe1 小时前
c++二叉树详解
数据结构·c++·算法
执笔论英雄1 小时前
【cuda】 pinpaged
android·java·数据库
崇山峻岭之间1 小时前
matlab的FOC仿真
开发语言·matlab
默默学前端1 小时前
JavaScript 中 call、apply、bind 的区别
开发语言·前端·javascript
ZhengEnCi1 小时前
Linux基础技术专栏
linux
一招定胜负1 小时前
大模型的API调用
数据库
星辰_mya2 小时前
Fork/Join 框架与并行流:CPU 密集型的“分身术”
java·开发语言·面试
czlczl200209252 小时前
插入时先写DB后写Redis?分布式中传统双写模式的缺陷
数据库·redis·分布式