一、基础背景:

这三个都是 C++11/17 标准库提供的「RAII 风格的锁封装器」,不是锁本身,而是对底层锁(mutex/shared_mutex)的安全封装,核心共性完全一致:

  1. 核心作用:自动加锁 + 自动解锁在作用域结束时(无论正常 return / 异常抛出),都会无条件解锁,彻底解决「手动调用lock()/unlock()遗漏解锁、异常导致死锁」的致命问题;
  2. 都不能独立使用:必须绑定一个底层互斥锁对象(std::mutex/std::shared_mutex等)才能工作;
  3. 都是栈上对象:锁的生命周期和作用域绑定,出作用域即解锁,安全无泄漏。

补充:底层的std::mutex是「锁本体」,是真正控制临界区互斥的核心;而这三个封装器是「锁的智能管家」,帮你安全的使用锁本体。

二、三者的核心区别

所有区别的根源,来自绑定的底层锁不同,先记 2 种核心锁本体:

  1. 排他锁(独占锁):std::mutex → 最基础的互斥锁,一把锁同时只能被一个线程持有,无论读 / 写,只要加锁,其他线程全部阻塞。所有读 / 写操作抢同一把锁,读多写少场景性能极差
  2. 读写锁:std::shared_mutex (C++17) → 核心设计:将锁分为「读锁」和「写锁」,是解决「读多写少」场景性能瓶颈的终极方案,也是你关心的「读写分离」核心依赖。
1. std::lock_guard —— 【极简轻量,无功能,极致高效】

C++11 提供,最简单、最轻量、零开销的 RAII 锁封装,是「能用则必用」的最优解,没有之一。

std::mutex mtx;
{
    std::lock_guard<std::mutex> lock(mtx); // 构造时:自动加锁
    // 临界区代码:读/写都可以
} // 

优点:编译期内联、无堆内存、无额外开销,性能和手动lock()+unlock()完全一致,最安全的「无脑加锁」方案;

缺点:功能极度简陋,灵活性为 0

  1. 只能在「构造时加锁」,「析构时解锁」,中间无法手动解锁
  2. 不支持「延迟加锁」「尝试加锁(try_lock)」「超时加锁」;
  3. 不支持「锁的所有权转移」(不能 move);
  4. 只能绑定 std::mutex 排他锁,不支持shared_mutex读写锁

适用场景

90% 的普通互斥场景,优先用 lock_guard —— 只要你的需求是「进入作用域加锁,出作用域解锁」,没有任何花里胡哨的需求,就用它。比如:单线程写、多线程读但不用分离、简单的临界区保护、计数器加减等。

2. std::unique_lock —— 【全能灵活,独占锁王者,功能无上限】

C++11 提供,功能最全、最灵活的 RAII 锁封装,是「独占锁」的终极形态,核心语义:对底层锁的「独占式所有权」

std::mutex mtx;
{
    std::unique_lock<std::mutex> lock(mtx); // 构造加锁,和lock_guard一致
    // 临界区代码:读/写都可以
    lock.unlock(); // 手动解锁(核心灵活点)
    // 其他代码...
    lock.lock();   // 再次手动加锁
} // 析构时:如果当前持有锁,自动解锁;未持有则无操作

核心优点:完全兼容 lock_guard 的所有功能,且拥有所有高级特性,是「独占锁」的超集:

  1. 支持「延迟加锁」:构造时不加锁,后续手动lock()std::unique_lock lock(mtx, std::defer_lock);
  2. 支持「尝试加锁」:lock.try_lock(),非阻塞,失败立即返回,不会死等;
  3. 支持「超时加锁」:lock.try_lock_for(std::chrono::seconds(1)),超时自动放弃;
  4. 支持「手动解锁 / 重加锁」:中间可以灵活释放锁,减少临界区范围,提升并发;
  5. 支持「锁的所有权转移」:可以用std::move()转移锁的所有权,适配函数返回值、lambda 捕获;
  6. 重中之重:唯一能配合std::condition_variable(条件变量)使用的锁封装器,muduo/Reactor 网络模型中必备;
  7. 可以绑定 std::mutex 排他锁,也可以绑定 std::shared_mutex 作为写锁

缺点:比 lock_guard 多一点点性能开销(一个 bool 标记位 + 少量成员函数),但在现代编译器下几乎可以忽略不计。

语义:

unique = 独占 → 只要unique_lock持有锁,其他任何线程(无论读 / 写)都无法获取该锁,这是「排他锁」的核心特征。

3. std::shared_lock —— 【只读专用,共享锁专属,读写分离核心】

C++14 提供,C++17 正式完善,专为「读操作」设计的 RAII 锁封装,核心语义:对底层锁的「共享式所有权」只能作为「读锁」使用,绝对不能做写操作

std::shared_mutex smtx;
{
    std::shared_lock<std::shared_mutex> lock(smtx); // 构造时:加【共享读锁】
    // 临界区代码:只能执行【读操作】!!!
} // 析构时:自动解锁

核心优点:共享加锁,读线程之间互不阻塞 —— 这是读写分离的性能核心:

  1. 多个线程可以同时持有同一个shared_mutex的共享读锁,线程之间完全不阻塞,并发读的效率拉满;
  2. 轻量、安全,RAII 自动解锁,和 lock_guard 一样省心;
  3. 支持「延迟加锁、尝试加锁、超时加锁」等高级特性,和 unique_lock 对齐。

缺点:

  1. 只能绑定std::shared_mutex读写锁,不能绑定普通的std::mutex
  2. 硬性规则:持有 shared_lock 时,绝对不能修改临界区数据,只能读!否则数据竞争、程序崩溃,这是语法不禁止但逻辑必须遵守的铁律。

语义:

shared = 共享 → 只要是「读操作」,所有线程都可以共享这把锁,只有「写操作」会被阻塞,这是「读写锁」的核心特征。

三、读写锁核心法则:shared_mutex + shared_lock(读) + unique_lock(写)

  • 读共享:多个线程用shared_lock加读锁 → 线程之间互不阻塞,可以同时读临界区数据;
  • 写独占:单个线程用unique_lock加写锁 → 阻塞所有其他线程(包括所有读线程和其他写线程),只有当前写线程能操作数据;
  • 写优先于读:如果有线程正在等待写锁,那么后续的读锁请求会被阻塞,直到写锁释放;避免「写线程饿死」。

这个设计是为了解决业务开发中 90% 的场景:读多写少 —— 比如:

  • HttpServer 的静态配置读取(如端口、域名)、缓存数据查询、日志读取、数据库查询结果缓存;
  • 所有「读操作远多于写操作」的场景。

性能对比:普通mutex排他锁,1000 个读线程会串行执行,耗时极长;而shared_mutex的共享读锁,1000 个读线程可以并行执行,性能提升百倍甚至千倍,写操作只是偶尔阻塞,完全不影响核心并发。

代码示例:

#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <thread>
#include <vector>
#include <string>

// 全局共享数据:读多写少的典型场景
std::string g_config = "default:8080"; 
std::shared_mutex g_smtx; // 读写锁本体,唯一的核心锁

// 读操作:必须用 shared_lock
std::string get_config() {
    std::shared_lock<std::shared_mutex> lock(g_smtx); // 共享读锁
    return g_config; // 只读,无修改
}

// 写操作:必须用 unique_lock
void set_config(const std::string& new_config) {
    std::unique_lock<std::shared_mutex> lock(g_smtx); // 独占写锁
    g_config = new_config; // 只写,修改数据
}

int main() {
    // 100个读线程:并行执行,互不阻塞
    std::vector<std::thread> read_threads;
    for (int i = 0; i < 100; ++i) {
        read_threads.emplace_back([](){
            std::cout << "读线程:" << get_config() << std::endl;
        });
    }

    // 1个写线程:阻塞所有读线程,写完后释放
    std::thread write_thread([](){
        set_config("update:8081");
        std::cout << "写线程:配置已更新" << std::endl;
    });

    for (auto& t : read_threads) t.join();
    write_thread.join();
    return 0;
}

四、开发选型

场景 1:普通互斥(无读写分离,只有「排他锁」需求)

需求:临界区既有读又有写,或者读写频率相当,只需要保证互斥即可。

  • 优先用 std::lock_guard<std::mutex>:只要能满足需求,就用它,极致高效、代码简洁;
  • 特殊情况用 std::unique_lock<std::mutex>:需要手动解锁、延迟加锁、条件变量、超时锁等高级功能时,才替换成它。

一句话总结:lock_guardunique_lock 的「精简高效版」,unique_locklock_guard 的「全能升级版」。

场景 2:读写分离(读多写少,核心性能优化需求)

需求:临界区大部分是读操作,极少是写操作,需要极致的并发读性能。

  • std::shared_mutex + 读用shared_lock + 写用unique_lock
  • 绝对禁忌:不要用lock_guard/unique_lock做读操作,会把共享读变成独占读,彻底丧失并发性能;不要用shared_lock做写操作,会导致数据竞争。
追问 1:unique_locklock_guard 灵活,为什么不全都用 unique_lock

因为lock_guard零开销的,编译器会把它完全内联,生成的汇编代码和手动lock()+unlock()一模一样;而unique_lock有一个bool成员变量标记锁的状态,还有一些成员函数,会产生极少量的开销。在高性能场景下,能省则省,简单场景用lock_guard是最优解。

追问 2:shared_mutex 的写锁为什么要用 unique_lock 而不是 lock_guard

可以用lock_guard<std::shared_mutex>做写锁,语法上完全合法,也能保证独占;但实际开发中,写操作往往需要更灵活的控制(比如手动解锁、超时加锁),而且unique_lock是行业标准写法,可读性更强,能明确标识「这是写锁」。

追问 3:有没有可能多个shared_lock导致数据不一致?

不会。因为shared_lock的核心是「只读」,所有持有共享读锁的线程都不会修改临界区数据,只是读取,所以数据永远是一致的;只有当有线程持有unique_lock写锁时,才会修改数据,而写锁会阻塞所有读锁,所以修改期间不会有读操作,数据一致性完全由锁保证。

追问 4:muduo 库中用的是哪种锁封装?

muduo 库中大量使用std::unique_lock,因为 muduo 是事件驱动的网络库,需要配合条件变量(Condition)实现线程间通信,而unique_lock是唯一能配合条件变量的锁封装;对于简单的临界区,muduo 也会用自定义的轻量锁封装,等价于lock_guard;muduo 也支持读写锁,读操作用共享锁,写操作用独占锁,和 C++ 标准一致。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐