锁的基础知识
本文最后更新于 198 天前,其中的信息可能已经有所发展或是发生改变。

主要讨论 锁的分类,锁的特点,锁的应用场景

为什么要有锁?为了解决多线程环境下的数据竞争问题,保证共享资源在同一时刻只有一个线程访问。

常见的锁

锁类型特点适用场景优点缺点
互斥锁 (Mutex)一次只允许一个线程访问最常见的线程同步简单、可靠线程阻塞、切换开销大
自旋锁 (Spinlock)线程忙等(不停循环检查锁)锁持有时间非常短上下文切换开销小CPU空转浪费资源
读写锁 (RWLock)多读单写读多写少的情况读操作并发高效写时仍需阻塞全部读
递归锁 (Recursive Mutex)同一线程可多次加锁避免死锁的特定场景编码方便滥用易隐藏设计问题
信号量 (Semaphore)允许多个线程同时访问有限资源限流、连接池可控制并发量复杂度较高
条件变量 (Condition Variable)等待某条件满足才继续线程间事件通知高效等待配合锁使用,设计复杂
读复制更新锁 (RCU)读几乎无锁,写延迟更新高并发读多写少系统极高读性能实现复杂,适合内核/底层
乐观锁 (Optimistic Lock)假设冲突少,冲突再重试数据库、CAS场景无锁开销低冲突多会频繁重试
悲观锁 (Pessimistic Lock)假设冲突多,先锁再操作银行转账、库存扣减简单安全性能可能差

互斥锁和自旋锁

上下文切换(Context Switch

  • 是指操作系统内核把 CPU 从一个线程/进程切换到另一个线程/进程时,保存和恢复寄存器、栈指针等上下文的过程。
  • 上下文切换昂贵的(开销在微秒级)。

互斥锁mutex

  • 如果加锁时锁已被占用,线程会主动让出 CPU,进入阻塞状态,等待被唤醒。
  • 涉及内核态调度,必然会有上下文切换。

自旋锁(spinlock)

  • 如果锁被占用,线程会忙等(自旋,不会立刻让出 CPU。
  • 无阻塞、无系统调度,所以正常情况下不会有上下文切换(除非内核抢占或者线程用尽时间片)。

加解锁时的上下文切换分析

锁类型情况上锁(lock)时上下文切换次数解锁(unlock)时上下文切换次数说明
互斥锁锁空闲0 次0 次用户态快速获取和释放锁,无需调度。
锁被占用2 次(当前线程阻塞)0~1 次(唤醒等待线程)当前线程睡眠时:
1. 线程切换到内核,阻塞当前线程 → 1 次
2. 切换到其他可运行线程 → 1 次
解锁时,可能需要唤醒等待线程,调度器决定是否切换 → 0 或 1 次。
自旋锁锁空闲0 次0 次自旋锁只用原子操作。
锁被占用0 次(自旋等待)0 次自旋锁不会主动让出 CPU,线程会一直忙等,不发生上下文切换(除非时间片耗尽或内核抢占)。

总结规律

  • 互斥锁
    • 成功获取锁时几乎没开销。
    • 如果锁被占用,线程会进入内核阻塞,至少发生 2 次上下文切换(阻塞自己、调度别人(若绑定的cpu亲和则另说);等锁释放再唤醒自己)。
    • 最通用且最基础的锁。
  • 自旋锁
    • 不会因为锁被占用而上下文切换,线程会忙等。
    • 但会浪费 CPU 时间片。
    • 常用于高性能编程中预期等待时间较少的竞争场景

代码验证

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <fstream>
#include <chrono>
#include <string>

// 读取当前进程的上下文切换次数
void print_context_switches(const std::string& tag) {
    std::ifstream status("/proc/self/status");
    std::string line;
    std::string voluntary, nonvoluntary;

    while (std::getline(status, line)) {
        if (line.find("voluntary_ctxt_switches") != std::string::npos) {
            voluntary = line;
        } else if (line.find("nonvoluntary_ctxt_switches") != std::string::npos) {
            nonvoluntary = line;
        }
    }

    std::cout << "[" << tag << "] " << voluntary << ", " << nonvoluntary << std::endl;
}

// 互斥锁测试
void test_mutex() {
    std::mutex mtx;
    bool ready = false;

    auto worker = [&]() {
        while (!ready) std::this_thread::yield();
        auto start = std::chrono::high_resolution_clock::now();

        std::lock_guard<std::mutex> lock(mtx);
        auto end = std::chrono::high_resolution_clock::now();
        std::cout << "[Mutex] Lock acquired after "
                  << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
                  << " us\n";
        print_context_switches("Mutex thread");
    };

    std::thread t(worker);

    mtx.lock();
    ready = true;
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 持锁100ms
    mtx.unlock();
    t.join();
}

// 自旋锁测试
void test_spinlock() {
    std::atomic_flag spin = ATOMIC_FLAG_INIT;
    bool ready = false;

    auto worker = [&]() {
        while (!ready) std::this_thread::yield();
        auto start = std::chrono::high_resolution_clock::now();

        while (spin.test_and_set(std::memory_order_acquire)) {
            // busy wait
        }

        auto end = std::chrono::high_resolution_clock::now();
        std::cout << "[Spinlock] Lock acquired after "
                  << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
                  << " us\n";
        spin.clear(std::memory_order_release);
        print_context_switches("Spinlock thread");
    };

    std::thread t(worker);

    spin.test_and_set(std::memory_order_acquire);
    ready = true;
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 持锁100ms
    spin.clear(std::memory_order_release);
    t.join();
}

int main() {
    std::cout << "=== Mutex test ===" << std::endl;
    test_mutex();
    std::cout << "\n=== Spinlock test ===" << std::endl;
    test_spinlock();
    return 0;
}

g++ -std=c++17 -O2 -pthread lock_test.cpp -o lock_test

./lock_test

我们可以通过getrusage方法获取当前线程的调度信息:

  • voluntary: 线程上下文主动切换计数
  • nonvoluntary: 由内核调度的线程上下文切换计数

在互斥锁的测试中,我们期望其是主调调度进内核,而测试自旋锁时,我们希望它只会被内核调度出。

测试结果与预期相符

在多线程自旋锁的测试中,其确实会消耗大量的cpu时间

taskset -c 6 ./lock_test_mt

perf top -C 6 -g

读写锁和RCU

读写锁(RWLock) 和 RCU(read-copy-update), 都适用于读多写少的场景,其主要的区别在于

  • RWLock: 允许同时读,但是写者与任何读者都互斥,写时会阻塞
  • RCU: 读时几乎无锁,写者复制新版本——原子发布——等待宽限期回收旧版本,读者可能读到稍旧的快照。

维度对比

维度RWLockRCU
读路径获取共享锁(有锁操作)进入读段(轻量标记/屏障,无锁或近似无锁)
写路径独占锁,等待所有读者退出后写复制新数据并原子切换指针;不阻塞在读者上
读可见性读到最新且一致的数据读到一致但可能过期的快照
延迟特性写会阻塞新读者;大量读者时写者可能饥饿写的发布很快,但回收要等宽限期;读基本恒定低延迟
扩展性(多核多读)锁元数据争用显著,扩展性一般极佳,读路径几乎不触碰共享状态
实现复杂度低(库现成,易用)高(版本管理、内存回收、内存屏障)
内存开销基本无额外副本写时需要副本/多版本,并有延迟回收
适用数据结构通用指针/引用为主的结构(表、链表、树、map 等)更自然
一致性需求强一致(写后读立即可见)最终一致(允许短暂陈旧)
写比例较高时容易退化(写阻塞读)复制成本高、回收压力大,收益变小

适用场景

RWLock 当你需要:

  • 读必须看到最新结果(强一致)
  • 写不频繁且可接受写期间暂停读
  • 代码要简单可维护,团队对并发技巧不熟

RCU 当你需要:

  • 极高读吞吐与低读延迟(热点读、多核扩展)
  • 读能接受**“稍旧也行”**的快照一致性
  • 数据结构以指针/引用为主,更新可以复制新版本再切换
  • 系统允许延迟回收与适度的内存放大

RWLock

读优先和写优先

读写锁可以设置读优先和写优先

策略行为特点优点缺点适用场景
读优先有写者等待时仍允许新读者进入读延迟低,读吞吐量极高写者可能长时间饥饿读极多写极少的场景
写优先有写者等待时阻塞新读者避免写饥饿,写延迟可控读者延迟变高,读吞吐下降写操作时效性要求高
公平策略按到达顺序排队无饥饿,性能均衡吞吐量略低于偏向策略需要整体公平调度的系统

总结选择建议

  • 读极多写极少:读优先,最大化读吞吐。
  • 写需要及时生效:写优先,避免写者饥饿。
  • 公平性重要或读写比例均衡:公平策略(例如 Java 的 ReentrantReadWriteLock(true) 构造公平锁)。
  • 实际应用:很多标准库(如 std::shared_mutex)是读优先,但数据库和高实时系统往往倾向写优先或公平策略。

递归锁

递归锁的特点是:同一个线程可以多次获得同一把锁,每次获得都会增加一个计数;只有当相同线程调用对应次数的 unlock 后,锁才真正释放给其他线程。 换句话说:递归锁允许重入——当持锁线程在临界区内调用会再次尝试加同一把锁的函数时,不会死锁,而是增加计数并继续执行。

比较罕见的锁类型

为什么需要(典型场景)

  • A 函数加锁后调用 B,B 也可能间接调用到 A(或其他也加同一把锁的函数),如果使用普通互斥锁,会自死锁;递归锁能避免这种“自己给自己锁住”的情况。
  • 常见于面向对象的实例方法相互调用,且每个方法都保护同一份状态时(e.g. 对象方法用锁保护成员变量)。

代码验证

#include <iostream>
#include <thread>
#include <mutex>

//std::mutex mtx; // 普通互斥锁,替换为 std::recursive_mutex 可解决重入
std::recursive_mutex mtx;

void inner(int n) {
    std::lock_guard<std::recursive_mutex> lk(mtx); // 若 mtx 是 std::mutex,第二次 lock 将死锁
    std::cout << "inner " << n << " locked by thread " << std::this_thread::get_id() << "\n";
    if (n > 0) inner(n - 1);
}

int main() {
    std::thread t([]{
        // 如果 mtx 是 std::mutex,这里第一次 lock 后,inner 再次 lock 会死锁。
        // 将上方 mtx 改成 std::recursive_mutex 并把 lock_guard 类型也改为 std::lock_guard<std::recursive_mutex>
        inner(2);
    });
    t.join();
    std::cout << "done\n";
}

互斥锁:

递归锁:

信号量和条件变量

这个我认为严格意义上不算锁吧,一者是资源的限制,一者是事件的通知机制。暂且不表。

乐观锁和悲观锁

  • 乐观锁:假设冲突很少,要操作数据时先不上锁。由于乐观锁全程不加锁,也可叫做无锁编程
  • 悲观锁: 假设冲突很多,操作前先上锁
维度悲观锁乐观锁
成本(读路径)读可能被阻塞,锁开销读无锁,低延迟
成本(写路径)写时独占,开销直观写要做冲突检测与重试
并发吞吐写多/冲突多时更稳健写少时吞吐高,写多时退化
一致性强(写期间无并发干扰)最终一致/基于版本校验
实现复杂度简单直接逻辑上需要重试/回退和版本管理
死锁风险存在(要注意锁顺序)无锁或少锁,死锁风险低
资源占用可能长时间持有锁阻塞他人可能多次复制/重试导致 CPU/内存开销

相关场景

悲观锁:适合并发冲突多,写多读少的场景

START TRANSACTION;
-- 锁定行,直到事务提交/回滚
SELECT balance FROM account WHERE id = 123 FOR UPDATE;

-- 进行更新
UPDATE account SET balance = balance - 100 WHERE id = 123;

COMMIT;

乐观锁:数据库,在线文档,svn,git

-- 读
SELECT balance, version FROM account WHERE id = 123;

-- 假设读到 version = 4,尝试更新时带版本条件
UPDATE account
SET balance = balance - 100, version = version + 1
WHERE id = 123 AND version = 4;

-- 若受影响行数 == 0 => 有并发写,检测到冲突,需要重试或报错

总结规律

  1. 冲突少,就用乐观锁. 不加锁,用版本号或者时间戳校验,性能高。
  2. 冲突多,就用悲观锁. 直接上锁,不会失败,就是性能低。

stl中有哪些锁

名称头文件特点用途
std::mutex<mutex>普通互斥锁,不可递归基本线程同步,保护共享资源
std::recursive_mutex<mutex>允许同一线程多次加锁避免递归调用时死锁
std::timed_mutex<mutex>支持 try_lock_for 和 try_lock_until限时尝试加锁
std::recursive_timed_mutex<mutex>递归 + 限时加锁灵活超时控制
std::shared_mutex (C++17)<shared_mutex>多读单写锁读多写少场景
std::shared_timed_mutex (C++14)<shared_mutex>共享锁+限时锁C++17前的过渡版本
std::lock_guard<mutex>简单的RAII加锁器作用域锁,自动解锁
std::unique_lock<mutex>更灵活的RAII锁可延迟上锁、解锁、转移
std::scoped_lock (C++17)<mutex>同时锁多个互斥量死锁安全的多锁方案
std::condition_variable<condition_variable>线程等待/通知结合 std::unique_lock 用
std::condition_variable_any<condition_variable>支持任意锁类型比 condition_variable 灵活
std::once_flag + std::call_once<mutex>保证只执行一次单例模式初始化
std::future / std::promise<future>异步结果同步任务结果传递
std::atomic<atomic>原子操作无锁并发
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇