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

理解并发编程:自旋锁、互斥锁与读写锁的解析

理解并发编程:自旋锁、互斥锁与读写锁的解析


摘要: 在多线程编程中,确保数据一致性和线程安全是至关重要的。锁是实现这一目标的核心机制之一。本文将深入探讨三种常见的锁:自旋锁(Spinlock)、互斥锁(Mutex)和读写锁(Read-Write Lock),分析它们的原理、特点、优缺点以及适用场景,并提供C++代码示例,帮助你更好地理解和运用它们。


目录

  • 1. 引言
  • 2. 自旋锁 (Spinlock)
    • 2.1 什么是自旋锁?
    • 2.2 特点
    • 2.3 优缺点
    • 2.4 适用场景
    • 2.5 C++代码示例
  • 3. 互斥锁 (Mutex)
    • 3.1 什么是互斥锁?
    • 3.2 特点
    • 3.3 优缺点
    • 3.4 适用场景
    • 3.5 C++代码示例
  • 4. 读写锁 (Read-Write Lock)
    • 4.1 什么是读写锁?
    • 4.2 特点
    • 4.3 优缺点
    • 4.4 适用场景
    • 4.5 C++代码示例
  • 5. 总结与对比

1. 引言

在并发编程中,多个线程可能会同时访问和修改同一个共享资源,这很容易导致数据竞争(Data Race)和不一致的问题。为了解决这个问题,我们需要一种同步机制来保证在任何时刻,只有一个线程能够访问临界区(Critical Section)。锁(Lock)就是最常用的一种同步原语。

选择合适的锁对程序的性能至关重要。本文将详细介绍自旋锁、互斥锁和读写锁这三种机制,帮助你根据不同的业务场景做出最优选择。

2. 自旋锁 (Spinlock)

2.1 什么是自旋锁?

自旋锁是一种非阻塞锁。当一个线程尝试获取一个已经被其他线程持有的自旋锁时,该线程并不会被挂起(即不会进入睡眠状态),而是会进入一个忙等待(Busy-Waiting)的循环,不断地检查锁是否已经被释放。一旦锁被释放,它会立即获取该锁并继续执行。

2.2 特点
  • 忙等待:未获取到锁时,线程会持续占用CPU时间片,反复检查锁的状态。
  • 非阻塞:线程不会从运行状态切换到睡眠状态,避免了线程上下文切换的开销。
  • 实现简单:通常基于原子操作(如CAS - Compare-And-Swap)实现。
2.3 优缺点
  • 优点
    • 响应速度快:一旦锁可用,能立刻获得,没有线程唤醒的延迟。
    • 避免上下文切换:对于锁持有时间极短的场景,避免了昂贵的线程上下文切换开销,性能更高。
  • 缺点
    • 消耗CPU:在锁被持有的时间内,等待的线程会持续消耗CPU资源,造成浪费。
    • 可能导致死锁:如果一个持有自旋锁的线程在锁释放前被阻塞(例如,发生缺页中断或被更高优先级的线程抢占),其他等待该锁的线程会一直自旋,可能耗尽CPU时间片,甚至导致系统崩溃。
2.4 适用场景

自旋锁适用于那些锁持有时间非常短临界区代码执行速度极快的场景。例如,对一个计数器进行原子增减操作。如果锁的持有时间预估会很长,或者线程竞争非常激烈,使用自旋锁会导致CPU效率大幅降低。

2.5 C++代码示例

C++标准库中没有直接提供自旋锁,但我们可以通过 std::atomic_flag 来模拟一个简单的自旋锁。

#include <iostream>
#include <thread>
#include <vector>
#include <atomic>class Spinlock {
private:std::atomic_flag flag = ATOMIC_FLAG_INIT;public:void lock() {// test_and_set:测试并设置标志位// 如果标志位是 false,则将其设置为 true 并返回 false// 如果标志位是 true,则不做任何事并返回 true// 当循环退出时,表示成功获取了锁while (flag.test_and_set(std::memory_order_acquire)) {// 忙等待}}void unlock() {// 清除标志位,表示锁已释放flag.clear(std::memory_order_release);}
};Spinlock spin;
long long counter = 0;void task() {for (int i = 0; i < 100000; ++i) {spin.lock();counter++;spin.unlock();}
}int main() {std::vector<std::thread> threads;for (int i = 0; i < 10; ++i) {threads.emplace_back(task);}for (auto& t : threads) {t.join();}std::cout << "Counter: " << counter << std::endl;return 0;
}

3. 互斥锁 (Mutex)

3.1 什么是互斥锁?

互斥锁是一种阻塞锁。当一个线程尝试获取一个已经被其他线程持有的互斥锁时,操作系统会将该线程置于睡眠状态(挂起),并将其从调度队列中移除。该线程将不再消耗CPU时间。当锁被释放时,操作系统会从等待该锁的线程中选择一个来唤醒,使其进入就绪状态,等待CPU调度。

3.2 特点
  • 阻塞等待:未获取到锁时,线程会进入睡眠状态,让出CPU。
  • 上下文切换:线程的挂起和唤醒涉及两次上下文切换(用户态 -> 内核态 -> 用户态),这会带来一定的性能开销。
3.3 优缺点
  • 优点
    • 不消耗CPU:等待的线程不会空转,节省了CPU资源,适用于锁竞争激烈或锁持有时间较长的场景。
  • 缺点
    • 性能开销大:线程的挂起和唤醒涉及昂贵的上下文切换,如果锁持有时间很短,这部分开销可能会超过临界区代码的执行时间。
    • 响应延迟:锁释放后,被唤醒的线程需要经过操作系统的调度才能重新执行,存在一定的延迟。
3.4 适用场景

互斥锁适用于锁持有时间较长临界区代码逻辑复杂线程竞争激烈的场景。在这种情况下,让线程睡眠以节省CPU是更明智的选择。

3.5 C++代码示例

C++11标准库提供了 std::mutex

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>std::mutex mtx;
long long counter = 0;void task() {for (int i = 0; i < 100000; ++i) {// std::lock_guard 在构造时加锁,析构时自动解锁,避免忘记解锁std::lock_guard<std::mutex> lock(mtx);counter++;}
}int main() {std::vector<std::thread> threads;for (int i = 0; i < 10; ++i) {threads.emplace_back(task);}for (auto& t : threads) {t.join();}std::cout << "Counter: " << counter << std::endl;return 0;
}

4. 读写锁 (Read-Write Lock)

4.1 什么是读写锁?

读写锁是一种更细粒度的锁,它将对共享资源的访问分为读操作写操作。它允许多个线程同时进行读操作,但在任何时候只允许一个线程进行写操作。当有线程在进行写操作时,其他所有线程(无论是读还是写)都必须等待。

4.2 特点
  • 读共享:多个读线程可以同时持有读锁,并发访问共享资源。
  • 写独占:写锁是排他的,当一个线程持有写锁时,其他任何线程都不能获取读锁或写锁。
  • 写优先(通常实现):当一个写线程在等待时,后续的读请求可能会被阻塞,以防止写线程“饿死”。
4.3 优缺点
  • 优点
    • 提高并发性:在“读多写少”的场景下,允许多个读线程并发执行,大大提高了程序的吞吐量。
  • 缺点
    • 实现复杂:相比于互斥锁,读写锁的内部状态管理更复杂。
    • 可能导致写线程饿死:如果读请求持续不断,写线程可能长时间无法获取到写锁。因此,很多读写锁的实现会采用“写优先”策略来缓解这个问题。
4.4 适用场景

读写锁非常适用于读操作远多于写操作的场景,例如缓存系统、数据库查询等。如果读写操作频率相当,使用读写锁可能不会带来性能提升,甚至可能因为其内部实现的复杂性而导致性能下降。

4.5 C++代码示例

C++17标准库提供了 std::shared_mutex 作为读写锁。

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <shared_mutex>std::shared_mutex rw_mutex;
long long counter = 0;void reader(int id) {for (int i = 0; i < 5; ++i) {// 获取共享锁(读锁)std::shared_lock<std::shared_mutex> lock(rw_mutex);std::cout << "Reader " << id << " reads counter: " << counter << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}void writer(int id) {for (int i = 0; i < 5; ++i) {// 获取独占锁(写锁)std::unique_lock<std::shared_mutex> lock(rw_mutex);counter++;std::cout << "Writer " << id << " writes counter: " << counter << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}int main() {std::vector<std::thread> threads;// 5个读线程for (int i = 0; i < 5; ++i) {threads.emplace_back(reader, i + 1);}// 2个写线程for (int i = 0; i < 2; ++i) {threads.emplace_back(writer, i + 1);}for (auto& t : threads) {t.join();}return 0;
}

5. 总结与对比

特性自旋锁 (Spinlock)互斥锁 (Mutex)读写锁 (Read-Write Lock)
锁类型非阻塞锁阻塞锁复合锁 (读共享,写独占)
等待策略忙等待 (Busy-Waiting)睡眠等待 (Sleeping)读请求并发,写请求阻塞
CPU消耗等待时消耗CPU等待时不消耗CPU取决于读写竞争情况
开销低 (无上下文切换)高 (上下文切换)中等,实现比互斥锁复杂
适用场景临界区极小,竞争不激烈临界区较大,竞争激烈读多写少,高并发读
风险可能导致CPU空转、死锁性能开销较大可能导致写线程饿死

选择哪种锁并没有绝对的答案,关键在于深入分析业务场景,理解不同锁的开销和优势,并通过性能测试来验证你的选择。希望本文能帮助你更好地掌握并发编程中的锁机制。


文章转载自:

http://unrfPs7f.cyfsL.cn
http://TSKsyjqj.cyfsL.cn
http://HDXVXCIN.cyfsL.cn
http://vcoVY2yK.cyfsL.cn
http://pDoL8UYR.cyfsL.cn
http://GpsjqPIO.cyfsL.cn
http://zCmRo2Ga.cyfsL.cn
http://478Sv4PZ.cyfsL.cn
http://reqkv6cN.cyfsL.cn
http://o0VAE6l4.cyfsL.cn
http://EFtgm8x7.cyfsL.cn
http://M9GGySBu.cyfsL.cn
http://ylO88rUq.cyfsL.cn
http://0IC7OYGw.cyfsL.cn
http://ehoxrXPf.cyfsL.cn
http://Q6rcKT8a.cyfsL.cn
http://BPzperJc.cyfsL.cn
http://Z2kPg3GL.cyfsL.cn
http://4PYg22Qi.cyfsL.cn
http://bikL3QRI.cyfsL.cn
http://GG4FSedh.cyfsL.cn
http://Jex4oPRo.cyfsL.cn
http://P6g4P9g7.cyfsL.cn
http://erG4GF91.cyfsL.cn
http://2jCW6PJJ.cyfsL.cn
http://ZrtE3OwJ.cyfsL.cn
http://S68O7csO.cyfsL.cn
http://ZKnZfqsn.cyfsL.cn
http://97SubmA6.cyfsL.cn
http://fpkr0aUm.cyfsL.cn
http://www.dtcms.com/a/385946.html

相关文章:

  • Java 大视界 -- Java 大数据在智能安防视频监控系统中的视频内容理解与智能预警升级
  • 腾讯元宝 Java 中的 23 种设计模式(GoF 设计模式)
  • Excel:根据数据信息自动生成模板数据(多个Sheet)
  • hibernate和mybatis的差异,以及这种类似场景的优缺点和选择
  • 设计模式之:观察者模式
  • 【pycharm】ubuntu24.04 安装配置index-tts及webdemo快速上手
  • Java 设计模式——观察者模式:从 4 种写法到 SpringBoot 进阶
  • “光敏” 黑科技:杜绝手机二维码读取时的 NFC 误触
  • AIGC(生成式AI)试用 36 -- shell脚本(辅助生成)
  • 【计算机网络 | 第17篇】DNS资源记录和报文
  • Flowise安全外网访问指南:基于cpolar的隧道配置详解
  • MySQL OCP认证[特殊字符]Oracle OCP认证
  • Springboot使用Freemark模板生成XML数据
  • 【数据工程】 10. 半结构化数据与 NoSQL 数据库
  • HarmonyOS应用开发:深入ArkUI声明式开发与性能优化实践
  • Vue: 组件注册
  • 408考研计算机网络第38题真题解析(2024)
  • Uni-app 生命周期全解析
  • JavaEE开发技术(第一章:Servlet基础)
  • 【数据结构】跳表
  • 设计模式-桥接模式02
  • Linux 基础命令详解与学习笔记
  • 设计模式(C++)详解——桥接模式(2)
  • 鹧鸪云光储流程系统:以智能仓储管理,驱动项目高效协同
  • DIY Linux 桌面:WiFi 管理器
  • 从 Pump.fun「直播」看热点币的生与死
  • 《算法闯关指南:优选算法-双指针》--05有效三角形的个数,06查找总价值为目标值的两个商品
  • Java List 详解:从基础到进阶的全面指南
  • 【问题】自启动的容器在开机重启后 都退出了,未能正常启动
  • 苹果手机上有没有可以定时提醒做事的工具