• 文件浏览器
  • 000 《Folly 库知识框架》 001 《folly::Utility 权威指南》 002 《folly::Preprocessor 权威指南》 003 《Folly::Traits 权威指南:C++ 元编程的基石》 004 《Folly::ScopeGuard 权威指南:C++ 作用域资源管理利器》 005 《Folly Singleton 权威指南:从入门到精通》 007 《Folly Dynamic.h 权威指南:C++ 动态类型实战》 008 《Folly Optional.h 权威指南:从入门到精通》 009 《Folly Expected.h 权威指南》 010 《Folly Try.h 权威指南:C++ 异常处理的现代实践》 011 《Folly Variant.h 权威指南》 012 《folly::Vector 权威指南: 深度探索与实践》 013 《Folly Map 权威指南:从入门到精通》 014 《Folly Set 权威指南》 015 《Folly SmallVector 权威指南》 016 《Folly Allocator.h 权威指南:C++ 高性能内存管理深度解析》 017 《Folly Foreach.h 权威指南:从入门到精通》 018 《folly/futures 权威指南:Future 和 Promise 深度解析与实战》 019 《Folly Executor 权威指南:从入门到精通 (Folly Executor: The Definitive Guide from Beginner to Expert)》 020 《深入浅出 Folly Fibers:FiberManager 和 Fiber 权威指南》 021 《folly EventBase.h 编程权威指南》 022 《Folly Baton.h 权威指南:C++ 高效线程同步实战》 023 《深入探索 folly/Synchronized.h:并发编程的基石 (In-depth Exploration of folly/Synchronized.h: The Cornerstone of Concurrent Programming)》 024 《folly/SpinLock.h 权威指南:原理、应用与最佳实践》 025 《Folly SharedMutex.h 权威指南:原理、应用与实战》 026 《Folly AtomicHashMap.h 权威指南:从入门到精通》 027 《Folly/IO 权威指南:高效网络编程实战》 028 《folly/Uri.h 权威指南 (Folly/Uri.h: The Definitive Guide)》 029 《Folly String.h 权威指南:深度解析、实战应用与高级技巧》 030 《folly/Format.h 权威指南 (The Definitive Guide to folly/Format.h)》 031 《Folly Conv.h 权威指南:C++ 高效类型转换详解》 032 《folly/Unicode.h 权威指南:深入探索与实战应用》 033 《folly/json.h 权威指南》 034 《Folly Regex.h 权威指南:从入门到精通 (Folly Regex.h: The Definitive Guide from Beginner to Expert)》 035 《Folly Clock.h 权威指南:系统、实战与深度解析》 036 《folly/Time.h 权威指南:C++ 时间编程实战》 037 《Folly Chrono.h 权威指南》 038 《Folly ThreadName.h 权威指南:系统线程命名深度解析与实战》 039 《Folly OptionParser.h 权威指南》 040 《C++ Range.h 实战指南:从入门到专家》 041 《Folly File.h 权威指南:从入门到精通》 042 《Folly/xlog.h 权威指南:从入门到精通》 043 《Folly Trace.h 权威指南:从入门到精通 (Folly Trace.h: The Definitive Guide from Beginner to Expert)》 044 《Folly Demangle.h 权威指南:C++ 符号反解的艺术与实践 (Folly Demangle.h: The Definitive Guide to C++ Symbol Demangling)》 045 《folly/StackTrace.h 权威指南:原理、应用与最佳实践 (folly/StackTrace.h Definitive Guide: Principles, Applications, and Best Practices)》 046 《Folly Test.h 权威指南:C++ 单元测试实战 (Folly Test.h: The Definitive Guide to C++ Unit Testing in Practice)》 047 《《Folly Benchmark.h 权威指南 (Folly Benchmark.h: The Definitive Guide)》》 048 《Folly Random.h 权威指南:C++随机数生成深度解析》 049 《Folly Numeric.h 权威指南》 050 《Folly Math.h 权威指南:从入门到精通 (Folly Math.h: The Definitive Guide from Beginner to Expert)》 051 《Folly FBMath.h 权威指南:从入门到精通 (Folly FBMath.h: The Definitive Guide - From Beginner to Expert)》 052 《Folly Cursor.h 权威指南:高效数据读取与解析 (Folly Cursor.h Authoritative Guide: Efficient Data Reading and Parsing)》 053 《Folly与Facebook Thrift权威指南:从入门到精通 (Folly and Facebook Thrift: The Definitive Guide from Beginner to Expert)》 054 《Folly CPUThreadPoolExecutor.h 权威指南:原理、实践与高级应用》 055 《Folly HardwareConcurrency.h 权威指南:系统级并发编程基石》

    025 《Folly SharedMutex.h 权威指南:原理、应用与实战》


    作者Lou Xiao, gemini创建时间2025-04-17 02:21:20更新时间2025-04-17 02:21:20

    🌟🌟🌟本文案由Gemini 2.0 Flash Thinking Experimental 01-21创作,用来辅助学习知识。🌟🌟🌟

    书籍大纲

    ▮▮▮▮ 1. chapter 1: 走近 SharedMutex.h (Getting Started with SharedMutex.h)
    ▮▮▮▮▮▮▮ 1.1 并发世界中的锁 (Locks in the Concurrent World)
    ▮▮▮▮▮▮▮▮▮▮▮ 1.1.1 为什么需要锁 (Why Locks are Necessary)
    ▮▮▮▮▮▮▮▮▮▮▮ 1.1.2 互斥锁 vs 共享互斥锁 (Mutex vs Shared Mutex)
    ▮▮▮▮▮▮▮ 1.2 Folly SharedMutex.h 概览 (Overview of Folly SharedMutex.h)
    ▮▮▮▮▮▮▮▮▮▮▮ 1.2.1 Folly 库简介 (Introduction to Folly Library)
    ▮▮▮▮▮▮▮▮▮▮▮ 1.2.2 SharedMutex.h 的定位与特点 (Positioning and Features of SharedMutex.h)
    ▮▮▮▮▮▮▮ 1.3 SharedMutex 的基本使用 (Basic Usage of SharedMutex)
    ▮▮▮▮▮▮▮▮▮▮▮ 1.3.1 核心 API 快速上手 (Quick Start with Core APIs)
    ▮▮▮▮▮▮▮▮▮▮▮ 1.3.2 代码示例:保护共享资源 (Code Example: Protecting Shared Resources)
    ▮▮▮▮ 2. chapter 2: SharedMutex 原理与机制 (Principles and Mechanisms of SharedMutex)
    ▮▮▮▮▮▮▮ 2.1 SharedMutex 的内部结构 (Internal Structure of SharedMutex)
    ▮▮▮▮▮▮▮ 2.2 锁的获取与释放流程 (Lock Acquisition and Release Process)
    ▮▮▮▮▮▮▮ 2.3 性能分析:读写性能与竞争 (Performance Analysis: Read/Write Performance and Contention)
    ▮▮▮▮▮▮▮ 2.4 SharedMutex 与 std::shared_mutex 对比 (Comparison between SharedMutex and std::shared_mutex)
    ▮▮▮▮ 3. chapter 3: SharedMutex 高级应用 (Advanced Applications of SharedMutex)
    ▮▮▮▮▮▮▮ 3.1 超时锁与非阻塞锁 (Timeout Locks and Non-blocking Locks)
    ▮▮▮▮▮▮▮ 3.2 SharedMutex 与条件变量 (SharedMutex and Condition Variables)
    ▮▮▮▮▮▮▮ 3.3 实战案例:高并发缓存系统 (Practical Case: High-Concurrency Cache System)
    ▮▮▮▮▮▮▮ 3.4 设计模式:读写锁模式的应用 (Design Pattern: Application of Reader-Writer Lock Pattern)
    ▮▮▮▮ 4. chapter 4: SharedMutex 最佳实践与避坑指南 (Best Practices and Pitfalls of SharedMutex)
    ▮▮▮▮▮▮▮ 4.1 高效使用 SharedMutex 的技巧 (Tips for Efficiently Using SharedMutex)
    ▮▮▮▮▮▮▮ 4.2 避免死锁和活锁 (Avoiding Deadlocks and Livelocks)
    ▮▮▮▮▮▮▮ 4.3 性能调优与监控 (Performance Tuning and Monitoring)
    ▮▮▮▮▮▮▮ 4.4 常见错误与问题排查 (Common Mistakes and Troubleshooting)
    ▮▮▮▮ 5. chapter 5: SharedMutex API 全面解析 (Comprehensive API Analysis of SharedMutex)
    ▮▮▮▮▮▮▮ 5.1 构造函数与析构函数 (Constructors and Destructor)
    ▮▮▮▮▮▮▮ 5.2 共享锁相关 API (Shared Lock Related APIs)
    ▮▮▮▮▮▮▮▮▮▮▮ 5.2.1 lock_shared()
    ▮▮▮▮▮▮▮▮▮▮▮ 5.2.2 unlock_shared()
    ▮▮▮▮▮▮▮▮▮▮▮ 5.2.3 try_lock_shared()
    ▮▮▮▮▮▮▮ 5.3 排他锁相关 API (Exclusive Lock Related APIs)
    ▮▮▮▮▮▮▮▮▮▮▮ 5.3.1 lock()
    ▮▮▮▮▮▮▮▮▮▮▮ 5.3.2 unlock()
    ▮▮▮▮▮▮▮▮▮▮▮ 5.3.3 try_lock()
    ▮▮▮▮▮▮▮ 5.4 其他辅助 API (Other Auxiliary APIs)
    ▮▮▮▮ 6. chapter 6: SharedMutex 高级主题与展望 (Advanced Topics and Future Trends of SharedMutex)
    ▮▮▮▮▮▮▮ 6.1 SharedMutex 在大型系统中的应用 (Application of SharedMutex in Large-Scale Systems)
    ▮▮▮▮▮▮▮ 6.2 SharedMutex 的扩展与定制 (Extension and Customization of SharedMutex)
    ▮▮▮▮▮▮▮ 6.3 并发编程的未来趋势 (Future Trends in Concurrent Programming)


    1. chapter 1: 走近 SharedMutex.h (Getting Started with SharedMutex.h)

    1.1 并发世界中的锁 (Locks in the Concurrent World)

    1.1.1 为什么需要锁 (Why Locks are Necessary)

    在当今的计算机系统中,并发编程已经成为提升性能和响应速度的关键技术。无论是多核处理器还是分布式系统,允许多个任务同时执行可以显著提高资源利用率和系统吞吐量。然而,并发也带来了新的挑战,其中最核心的问题之一就是如何安全地管理对共享资源的访问

    想象一下一个简单的场景:多个线程同时尝试修改同一个变量。如果没有适当的同步机制,每个线程都可能读取到过时的值,或者在写入时覆盖其他线程的修改,从而导致数据竞争(Race Condition),最终使得程序行为变得不可预测,甚至产生灾难性的错误。

    例如,考虑一个银行账户的例子,账户余额是共享资源。如果两个线程同时执行存款操作,并且没有使用锁来保护账户余额,那么可能会发生以下情况:

    1. 线程 A 读取账户余额,假设为 100 元。
    2. 线程 B 也读取账户余额,此时也为 100 元。
    3. 线程 A 计算存款后的余额,例如存入 50 元,计算结果为 150 元。
    4. 线程 B 也计算存款后的余额,例如也存入 50 元,计算结果也为 150 元。
    5. 线程 A 将 150 元写回账户余额。
    6. 线程 B 也将 150 元写回账户余额。

    最终,尽管两个线程都存入了 50 元,但账户余额只增加了 50 元,而不是预期的 100 元。这就是典型的数据竞争问题,它源于多个线程并发地访问和修改共享资源,而没有采取适当的同步措施来保证操作的原子性。

    为了解决这类并发访问共享资源的问题,锁(Locks)应运而生。锁是一种同步机制,它允许程序员控制多个线程对共享资源的访问。通过使用锁,我们可以确保在同一时刻,只有一个线程可以访问被保护的共享资源,从而避免数据竞争,保证数据的一致性和程序的正确性。

    锁就像现实生活中的门锁,当一个线程想要访问共享资源时,它需要先获取锁,就像开门一样。一旦线程获得了锁,它就可以安全地访问共享资源。当线程完成对共享资源的访问后,它需要释放锁,就像锁门一样,以便其他线程可以获取锁并访问资源。

    总结来说,锁在并发编程中扮演着至关重要的角色,它们是保证数据安全和程序正确性的基石。没有锁的保护,并发程序很容易出现各种难以调试和复现的错误。因此,理解锁的概念和正确使用锁是编写高质量并发程序的关键。

    1.1.2 互斥锁 vs 共享互斥锁 (Mutex vs Shared Mutex)

    锁作为并发控制的重要工具,根据其不同的特性和应用场景,可以分为多种类型。其中,最常见的两种锁类型是互斥锁(Mutex)共享互斥锁(Shared Mutex)。理解这两种锁的区别和适用场景,对于编写高效且安全的并发程序至关重要。

    互斥锁(Mutex),全称 Mutual Exclusion Lock,是最基本也是最常用的一种锁。它的核心特性是独占性,即在任何时刻,最多只有一个线程可以持有互斥锁。当一个线程尝试获取已被其他线程持有的互斥锁时,该线程会被阻塞,直到持有锁的线程释放锁。互斥锁适用于读写操作都需要互斥的场景,例如,当多个线程需要同时修改同一个共享变量时,就需要使用互斥锁来保证修改操作的原子性。

    标准库 <mutex> 中提供的 std::mutex 就是典型的互斥锁。它提供了 lock()unlock() 等基本操作,用于获取和释放锁。

    共享互斥锁(Shared Mutex),也称为读写锁(Read-Write Lock),是对互斥锁的一种扩展。它在互斥锁的基础上,引入了共享模式独占模式两种锁模式,从而允许多个线程同时以共享模式持有锁,但只允许一个线程以独占模式持有锁。

    共享模式(Shared Mode),也称为读模式共享锁。当线程以共享模式获取锁时,表示该线程只需要读取共享资源,而不会修改它。多个线程可以同时以共享模式持有同一个共享互斥锁,从而实现并发读取,提高程序的并发性能。
    独占模式(Exclusive Mode),也称为写模式排他锁。当线程以独占模式获取锁时,表示该线程需要修改共享资源。在独占模式下,只有一个线程可以持有锁,其他任何线程(包括尝试以共享模式获取锁的线程)都会被阻塞,直到持有独占锁的线程释放锁。

    共享互斥锁适用于读多写少的场景。在这种场景下,多个线程可以并发地读取共享资源,而只有在需要修改资源时,才需要进行互斥访问。这样可以显著提高程序的并发性能,尤其是在读取操作远多于写入操作的情况下。

    folly/SharedMutex.h 以及 C++17 标准库中的 std::shared_mutex 都是共享互斥锁的实现。它们都提供了 lock_shared()unlock_shared()(用于共享模式)以及 lock()unlock()(用于独占模式)等 API。

    为了更清晰地对比互斥锁和共享互斥锁,我们可以用表格总结它们的区别:

    特性互斥锁(Mutex)共享互斥锁(Shared Mutex)
    锁模式独占模式共享模式 & 独占模式
    并发读不允许允许
    并发写不允许不允许
    适用场景读写互斥读多写少
    性能简单,开销较小复杂,读多场景下性能更优
    代表实现std::mutexfolly::SharedMutex, std::shared_mutex

    选择使用互斥锁还是共享互斥锁,需要根据具体的应用场景和对共享资源的访问模式来决定。如果读写操作都需要互斥,那么互斥锁是简单且有效的选择。如果读操作远多于写操作,并且希望提高并发读取性能,那么共享互斥锁是更合适的选择。在接下来的章节中,我们将深入探讨 folly/SharedMutex.h 的使用和原理,帮助读者更好地理解和应用共享互斥锁。

    1.2 Folly SharedMutex.h 概览 (Overview of Folly SharedMutex.h)

    1.2.1 Folly 库简介 (Introduction to Folly Library)

    在深入了解 folly/SharedMutex.h 之前,我们首先需要对 Folly 库有一个基本的认识。Folly(全称 Facebook Open-source Library)是由 Facebook 开源的一套 C++ 库。它旨在提供高效、实用、且经过良好测试的 C++ 组件,以解决实际工程中遇到的各种问题。Folly 并非一个大而全的框架,而更像是一个工具箱,其中包含了许多独立的、可复用的组件,涵盖了从基础数据结构、并发工具、网络编程到高性能计算等多个领域。

    Folly 库的设计哲学可以概括为以下几点:

    高性能(High Performance):Folly 库中的组件通常都经过精心的性能优化,力求在各种场景下都能达到最佳的性能表现。例如,Folly 提供了许多高性能的数据结构和算法,以及针对特定场景优化的并发工具。
    实用性(Practicality):Folly 库的设计目标是解决实际工程问题,因此它提供的组件都非常实用,可以直接应用于实际项目中。例如,Folly 提供了方便易用的 JSON 解析库、HTTP 客户端库、以及各种并发容器等。
    现代 C++(Modern C++):Folly 库大量使用了现代 C++ 的特性,例如 C++11/14/17 标准中的各种新特性,以及模板元编程、RAII 等技术。这使得 Folly 库的代码更加简洁、高效、且易于维护。
    良好的测试(Well-tested):Folly 库中的组件都经过了严格的单元测试和集成测试,保证了代码的质量和稳定性。Facebook 在其内部的大规模系统中广泛使用了 Folly 库,这也从侧面验证了 Folly 库的可靠性。

    Folly 库包含众多模块,其中一些比较重要的模块包括:

    Strings:提供了高性能的字符串处理工具,例如 fbstringStringPiece 等。
    Collections:提供了各种高效的数据结构,例如 fbvectorF14ValueMapConcurrentHashMap 等。
    Concurrency:提供了丰富的并发编程工具,例如 Future/PromiseExecutorEventCountSharedMutex 等。
    IO:提供了高性能的 IO 库,例如 SocketEventBaseAsyncSocket 等。
    JSON:提供了快速的 JSON 解析和生成库,json
    Logging:提供了灵活且高性能的日志库,logging

    folly/SharedMutex.h 就位于 Folly 库的 Concurrency 模块中,是 Folly 提供的众多并发工具之一。它为 C++ 开发者提供了一个强大且高效的共享互斥锁实现,可以用于构建高性能的并发程序。在接下来的内容中,我们将重点介绍 folly/SharedMutex.h 的定位、特点以及使用方法。

    1.2.2 SharedMutex.h 的定位与特点 (Positioning and Features of SharedMutex.h)

    folly/SharedMutex.h 在 Folly 库的并发工具集中占据着重要的地位。它提供了一个高性能、功能丰富的共享互斥锁实现,旨在解决多线程并发读写共享资源时的同步问题。与标准库中的 std::shared_mutex 相比,folly::SharedMutex 在某些方面具有独特的优势和特点。

    定位

    folly::SharedMutex 的主要定位是为需要细粒度读写锁控制的并发程序提供支持。在许多高性能系统中,例如缓存系统、数据库系统、以及高并发服务器等,读操作通常远多于写操作。在这种场景下,使用传统的互斥锁(如 std::mutex)会限制并发读取的性能,而 folly::SharedMutex 提供的共享模式锁可以允许多个线程同时读取共享资源,从而显著提高系统的并发吞吐量。

    folly::SharedMutex 适用于以下场景:

    读多写少的共享资源访问模式。
    ⚝ 需要高性能的读写锁实现。
    ⚝ 需要更丰富的功能,例如可中断的锁、升级锁等。

    特点

    folly::SharedMutex 相比于 std::shared_mutex,具有以下一些显著的特点:

    性能优化folly::SharedMutex 在设计和实现上都进行了大量的性能优化,尤其是在高并发读取的场景下,其性能通常优于 std::shared_mutex。这得益于 Folly 库一贯的性能至上的设计理念,以及对底层硬件和操作系统的深入理解。

    可中断的锁(Interruptible Locks)folly::SharedMutex 提供了可中断的锁获取操作。这意味着,当一个线程在等待获取锁的过程中,可以被其他线程中断,从而避免长时间的阻塞。这在某些需要响应中断信号的场景下非常有用。

    升级锁(Upgradeable Locks)folly::SharedMutex 支持升级锁的概念。升级锁允许一个线程先以共享模式获取锁,然后在需要写入时,将锁升级为独占模式,而无需先释放共享锁再重新获取独占锁。这可以减少锁的竞争和上下文切换,提高性能。

    更丰富的 APIfolly::SharedMutex 提供了比 std::shared_mutex 更丰富的 API,例如 try_lock_shared_for()try_lock_for() 等超时锁,以及 make_shared_lock_guard()make_unique_lock() 等方便的 RAII 锁管理工具。

    更好的跨平台兼容性:虽然 std::shared_mutex 是 C++17 标准的一部分,但不同编译器和标准库的实现可能存在差异。folly::SharedMutex 作为 Folly 库的一部分,经过了广泛的测试和验证,具有更好的跨平台兼容性和稳定性。

    总而言之,folly::SharedMutex 是一个强大且高效的共享互斥锁实现,它在性能、功能和易用性方面都具有优势。对于需要构建高性能并发程序的 C++ 开发者来说,folly::SharedMutex 是一个值得深入学习和使用的工具。在接下来的章节中,我们将逐步深入 folly::SharedMutex.h 的使用方法、原理以及高级应用。

    1.3 SharedMutex 的基本使用 (Basic Usage of SharedMutex)

    1.3.1 核心 API 快速上手 (Quick Start with Core APIs)

    要开始使用 folly::SharedMutex,首先需要确保你的项目中已经集成了 Folly 库。如果还没有集成,你需要参考 Folly 官方文档进行安装和配置。一旦 Folly 库集成完成,你就可以在你的 C++ 代码中包含头文件 <folly/SharedMutex.h>,然后就可以使用 folly::SharedMutex 类了。

    folly::SharedMutex 的核心 API 主要分为两类:共享锁 API排他锁 API

    共享锁 API 用于以共享模式获取和释放锁,允许多个线程同时持有共享锁,适用于读操作

    lock_shared():以共享模式获取锁。如果当前没有线程持有排他锁,则当前线程可以成功获取共享锁。如果已经有线程持有排他锁,则当前线程会被阻塞,直到排他锁被释放。
    unlock_shared():释放之前以共享模式获取的锁。
    try_lock_shared():尝试以共享模式获取锁。如果可以立即获取到共享锁,则返回 true;否则,立即返回 false,不会阻塞当前线程。
    try_lock_shared_for(const std::chrono::duration& timeout_duration):尝试在指定的时间段内以共享模式获取锁。如果在超时时间内成功获取到共享锁,则返回 true;否则,返回 false
    try_lock_shared_until(const std::chrono::time_point& timeout_time):尝试在指定的时间点之前以共享模式获取锁。如果在指定时间点之前成功获取到共享锁,则返回 true;否则,返回 false

    排他锁 API 用于以独占模式获取和释放锁,只允许一个线程持有排他锁,适用于写操作

    lock():以独占模式获取锁。如果当前没有线程持有任何锁(包括共享锁和排他锁),则当前线程可以成功获取排他锁。如果已经有线程持有锁,则当前线程会被阻塞,直到所有锁都被释放。
    unlock():释放之前以独占模式获取的锁。
    try_lock():尝试以独占模式获取锁。如果可以立即获取到排他锁,则返回 true;否则,立即返回 false,不会阻塞当前线程。
    try_lock_for(const std::chrono::duration& timeout_duration):尝试在指定的时间段内以独占模式获取锁。如果在超时时间内成功获取到排他锁,则返回 true;否则,返回 false
    try_lock_until(const std::chrono::time_point& timeout_time):尝试在指定的时间点之前以独占模式获取锁。如果在指定时间点之前成功获取到排他锁,则返回 true;否则,返回 false

    除了上述基本的锁操作 API,folly::SharedMutex 还提供了 RAII 风格的锁管理类,例如 folly::SharedLockGuardfolly::UniqueLock,可以方便地管理锁的生命周期,避免手动 unlock() 造成的错误,并确保在异常情况下锁也能被正确释放。

    folly::SharedLockGuard<SharedMutex>:用于管理共享锁的 RAII 类。在构造时尝试获取共享锁,在析构时自动释放共享锁。
    folly::UniqueLock<SharedMutex>:用于管理排他锁的 RAII 类。在构造时尝试获取排他锁,在析构时自动释放排他锁。folly::UniqueLock 更加灵活,可以控制锁的延迟获取和释放。

    快速上手示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <iostream>
    3 #include <thread>
    4 #include <chrono>
    5
    6 folly::SharedMutex sharedMutex;
    7 int sharedResource = 0;
    8
    9 void readerThread(int id) {
    10 for (int i = 0; i < 5; ++i) {
    11 // 使用 SharedLockGuard 自动管理共享锁
    12 folly::SharedLockGuard<folly::SharedMutex> lock(sharedMutex);
    13 std::cout << "Reader " << id << " reads: " << sharedResource << std::endl;
    14 std::this_thread::sleep_for(std::chrono::milliseconds(100));
    15 }
    16 }
    17
    18 void writerThread(int id) {
    19 for (int i = 0; i < 3; ++i) {
    20 // 使用 UniqueLock 自动管理排他锁
    21 folly::UniqueLock<folly::SharedMutex> lock(sharedMutex);
    22 sharedResource++;
    23 std::cout << "Writer " << id << " writes: " << sharedResource << std::endl;
    24 std::this_thread::sleep_for(std::chrono::milliseconds(200));
    25 }
    26 }
    27
    28 int main() {
    29 std::thread readers[3];
    30 std::thread writers[2];
    31
    32 for (int i = 0; i < 3; ++i) {
    33 readers[i] = std::thread(readerThread, i);
    34 }
    35 for (int i = 0; i < 2; ++i) {
    36 writers[i] = std::thread(writerThread, i);
    37 }
    38
    39 for (int i = 0; i < 3; ++i) {
    40 readers[i].join();
    41 }
    42 for (int i = 0; i < 2; ++i) {
    43 writers[i].join();
    44 }
    45
    46 return 0;
    47 }

    在这个示例中,我们创建了多个读者线程和写者线程,它们共享访问 sharedResource 变量。读者线程使用 folly::SharedLockGuard 获取共享锁进行读取操作,写者线程使用 folly::UniqueLock 获取排他锁进行写入操作。通过 folly::SharedMutex 的保护,我们可以确保在并发访问下,sharedResource 的数据一致性,并且允许多个读者线程同时读取,提高了并发性能。

    1.3.2 代码示例:保护共享资源 (Code Example: Protecting Shared Resources)

    为了更深入地理解 folly::SharedMutex 的基本使用,我们来看一个更完整的代码示例,模拟一个简单的线程安全计数器。这个计数器允许多个线程同时读取计数器的值,但只允许一个线程修改计数器的值。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <iostream>
    3 #include <thread>
    4 #include <vector>
    5
    6 class ThreadSafeCounter {
    7 public:
    8 ThreadSafeCounter() : count_(0) {}
    9
    10 unsigned int getCount() const {
    11 // 共享模式读取计数器
    12 folly::SharedLockGuard<folly::SharedMutex> lock(mutex_);
    13 return count_;
    14 }
    15
    16 void increment() {
    17 // 排他模式修改计数器
    18 folly::UniqueLock<folly::SharedMutex> lock(mutex_);
    19 count_++;
    20 }
    21
    22 void decrement() {
    23 // 排他模式修改计数器
    24 folly::UniqueLock<folly::SharedMutex> lock(mutex_);
    25 count_--;
    26 }
    27
    28 private:
    29 mutable folly::SharedMutex mutex_; // mutable 允许在 const 方法中修改 mutex_
    30 unsigned int count_;
    31 };
    32
    33 void readerTask(ThreadSafeCounter& counter, int threadId) {
    34 for (int i = 0; i < 10; ++i) {
    35 std::cout << "Reader " << threadId << ": Count = " << counter.getCount() << std::endl;
    36 std::this_thread::sleep_for(std::chrono::milliseconds(50));
    37 }
    38 }
    39
    40 void writerTask(ThreadSafeCounter& counter, int threadId) {
    41 for (int i = 0; i < 5; ++i) {
    42 counter.increment();
    43 std::cout << "Writer " << threadId << ": Increment to " << counter.getCount() << std::endl;
    44 std::this_thread::sleep_for(std::chrono::milliseconds(100));
    45 }
    46 for (int i = 0; i < 3; ++i) {
    47 counter.decrement();
    48 std::cout << "Writer " << threadId << ": Decrement to " << counter.getCount() << std::endl;
    49 std::this_thread::sleep_for(std::chrono::milliseconds(150));
    50 }
    51 }
    52
    53 int main() {
    54 ThreadSafeCounter counter;
    55 std::vector<std::thread> threads;
    56
    57 // 创建 5 个读者线程
    58 for (int i = 0; i < 5; ++i) {
    59 threads.emplace_back(readerTask, std::ref(counter), i);
    60 }
    61 // 创建 2 个写者线程
    62 for (int i = 0; i < 2; ++i) {
    63 threads.emplace_back(writerTask, std::ref(counter), i);
    64 }
    65
    66 // 等待所有线程结束
    67 for (auto& thread : threads) {
    68 thread.join();
    69 }
    70
    71 std::cout << "Final Count: " << counter.getCount() << std::endl;
    72
    73 return 0;
    74 }

    在这个例子中,我们定义了一个 ThreadSafeCounter 类,它内部使用 folly::SharedMutex 来保护计数器 count_getCount() 方法使用 folly::SharedLockGuard 获取共享锁,允许多个读者线程同时读取计数器的值。increment()decrement() 方法使用 folly::UniqueLock 获取排他锁,保证在修改计数器时只有一个写者线程在执行。

    main() 函数中,我们创建了多个读者线程和写者线程,并发地访问和修改 ThreadSafeCounter 对象。通过运行这个程序,你可以观察到读者线程可以并发地读取计数器的值,而写者线程的修改操作是互斥的,从而保证了计数器的线程安全性。

    这个例子展示了 folly::SharedMutex 在保护共享资源、实现读写分离并发控制方面的基本应用。在后续章节中,我们将深入探讨 folly::SharedMutex 的原理、高级应用以及最佳实践,帮助读者更全面地掌握和运用这一强大的并发工具。

    END_OF_CHAPTER

    2. chapter 2: SharedMutex 原理与机制 (Principles and Mechanisms of SharedMutex)

    2.1 SharedMutex 的内部结构 (Internal Structure of SharedMutex)

    要深入理解 folly::SharedMutex 的工作原理,首先需要了解其内部结构。SharedMutex 的设计目标是在保证线程安全的前提下,尽可能地提高读操作的并发性能。为了实现这一目标,SharedMutex 内部维护了多个关键组件,共同协作完成锁的获取、释放以及线程同步等操作。

    SharedMutex 的核心结构可以概括为以下几个关键部分:

    状态变量 (State Variable)SharedMutex 使用一个或多个原子变量来维护锁的当前状态。这个状态变量记录了锁是被排他持有(写锁),还是被共享持有(读锁),以及是否有线程在等待获取锁。使用原子变量保证了状态更新的原子性,避免了数据竞争。

    等待队列 (Wait Queues):为了处理锁竞争的情况,SharedMutex 内部维护了等待队列。当线程尝试获取锁但锁已被其他线程持有,或者不满足获取条件时,该线程会被加入到等待队列中。SharedMutex 通常会使用不同的等待队列来区分等待获取读锁的线程和等待获取写锁的线程,以便在锁释放时能够有策略地唤醒等待线程,例如优先唤醒写线程以避免写饥饿问题。

    Futex 或类似的内核同步机制 (Futex or Similar Kernel Synchronization Mechanisms)SharedMutex 底层依赖于操作系统提供的原子操作和内核同步机制,例如 Linux 上的 futex(fast userspace mutex)。futex 允许在用户空间快速检查锁状态,只有在真正发生竞争时才陷入内核,从而减少了不必要的内核态切换开销,提高了性能。

    自旋优化 (Spinning Optimization):在某些情况下,线程在等待锁释放时,并不会立即进入休眠状态,而是会进行短暂的自旋等待。自旋是指线程在一个循环中不断地检查锁状态,而不是立即放弃 CPU 时间片。自旋的目的是在锁持有时间非常短的情况下,避免线程上下文切换的开销。自旋的策略需要 carefully tuned,过长的自旋会浪费 CPU 资源,而过短的自旋则可能错过快速释放的锁。

    可以用一个简化的示意图来表示 SharedMutex 的内部结构:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 +-----------------------+
    2 | SharedMutex |
    3 +-----------------------+
    4 | - State Variable | // 原子变量,记录锁状态
    5 | - Read Wait Queue | // 等待读锁的线程队列
    6 | - Write Wait Queue | // 等待写锁的线程队列
    7 | - Futex (or similar) | // 内核同步机制
    8 | - Spinning Logic | // 自旋优化逻辑
    9 +-----------------------+

    更具体地,SharedMutex 的状态变量可能使用一个整数来编码多种状态,例如:

    0: 锁空闲(既没有读锁也没有写锁)。
    正数: 表示当前持有读锁的线程数量。
    负数: 表示有线程持有写锁(通常用 -1 表示)。
    特定标志位: 可能用于表示是否有线程在等待写锁,或者是否有条件变量与锁关联等。

    通过巧妙地设计状态变量的编码方式,以及高效地管理等待队列和利用底层的同步机制,SharedMutex 能够在多种并发场景下提供良好的性能和可伸缩性。不同的 SharedMutex 实现可能会在细节上有所差异,例如等待队列的具体实现方式(FIFO 队列、优先级队列等),自旋的策略,以及状态变量的编码方式等,但其核心思想和基本组件是类似的。

    理解 SharedMutex 的内部结构有助于我们更好地理解其行为特性,并在实际应用中正确地使用和优化它。例如,了解等待队列的存在可以帮助我们理解锁竞争时线程的调度行为,而了解自旋优化则可以帮助我们理解在低竞争场景下 SharedMutex 的性能优势。

    2.2 锁的获取与释放流程 (Lock Acquisition and Release Process)

    SharedMutex 的核心操作是锁的获取和释放。根据锁的类型(共享锁/读锁和排他锁/写锁),其获取和释放流程有所不同。下面分别详细介绍读锁和写锁的获取与释放流程。

    ① 读锁的获取与释放流程 (Read Lock Acquisition and Release Process)

    多个线程可以同时持有读锁,这允许多个读者并发地访问共享资源。读锁的获取流程通常如下:

    1. 检查锁状态 (Check Lock State):线程尝试原子地检查 SharedMutex 的状态变量。
    2. 判断是否可以获取读锁 (Determine if Read Lock Can Be Acquired)
      ▮▮▮▮⚝ 如果当前没有线程持有写锁(状态变量不是负数),并且没有写线程在等待(某些实现可能会考虑写线程的优先级),则可以尝试获取读锁。
      ▮▮▮▮⚝ 如果当前已经有其他线程持有读锁(状态变量是正数),则可以直接增加读锁计数。
    3. 获取读锁 (Acquire Read Lock)
      ▮▮▮▮⚝ 如果可以获取读锁,则原子地增加状态变量中的读锁计数。这个操作通常使用原子加操作完成。
      ▮▮▮▮⚝ 如果状态变量更新成功,则线程成功获取读锁,可以继续访问共享资源。
    4. 处理竞争 (Handle Contention)
      ▮▮▮▮⚝ 如果无法立即获取读锁(例如,当前有线程持有写锁),则线程需要进入等待状态。
      ▮▮▮▮⚝ 线程通常会被加入到读等待队列中,并调用底层的 futex 等系统调用进入休眠状态,等待锁释放的通知。

    读锁的释放流程相对简单:

    1. 原子地减少读锁计数 (Atomically Decrement Read Lock Count):线程完成对共享资源的读取后,需要释放读锁。这通常通过原子地减少状态变量中的读锁计数来实现。
    2. 检查是否需要唤醒等待线程 (Check if Waiting Threads Need to Be Notified)
      ▮▮▮▮⚝ 当读锁计数减少到 0 时,表示当前已经没有线程持有读锁。此时,需要检查是否有等待写锁的线程。
      ▮▮▮▮⚝ 如果有等待写锁的线程,则需要唤醒其中一个(或多个,取决于具体的唤醒策略)写线程,让其尝试获取写锁。唤醒操作通常通过 futex 等系统调用完成。

    ② 写锁的获取与释放流程 (Write Lock Acquisition and Release Process)

    写锁是排他锁,同一时刻只允许一个线程持有写锁,以保证对共享资源的独占访问。写锁的获取流程通常如下:

    1. 检查锁状态 (Check Lock State):线程尝试原子地检查 SharedMutex 的状态变量。
    2. 判断是否可以获取写锁 (Determine if Write Lock Can Be Acquired)
      ▮▮▮▮⚝ 只有当当前没有任何线程持有读锁或写锁(状态变量为 0)时,才可以尝试获取写锁。
    3. 获取写锁 (Acquire Write Lock)
      ▮▮▮▮⚝ 如果可以获取写锁,则原子地将状态变量设置为表示写锁被持有的状态(例如,设置为 -1)。这个操作通常使用原子交换或原子比较并交换操作完成。
      ▮▮▮▮⚝ 如果状态变量更新成功,则线程成功获取写锁,可以独占访问和修改共享资源。
    4. 处理竞争 (Handle Contention)
      ▮▮▮▮⚝ 如果无法立即获取写锁(例如,当前有线程持有读锁或写锁),则线程需要进入等待状态。
      ▮▮▮▮⚝ 线程通常会被加入到写等待队列中,并调用底层的 futex 等系统调用进入休眠状态,等待锁释放的通知。

    写锁的释放流程如下:

    1. 释放写锁 (Release Write Lock):线程完成对共享资源的修改后,需要释放写锁。这通常通过原子地将状态变量重置为 0 来实现。
    2. 检查是否需要唤醒等待线程 (Check if Waiting Threads Need to Be Notified)
      ▮▮▮▮⚝ 当写锁被释放后,需要检查是否有等待读锁或写锁的线程。
      ▮▮▮▮⚝ 唤醒策略通常会优先考虑写线程,以避免写饥饿。但也可能根据具体的实现策略,例如公平性策略,来决定唤醒读线程还是写线程。唤醒操作同样通过 futex 等系统调用完成。

    流程总结图示

    为了更清晰地理解读写锁的获取和释放流程,可以使用流程图来概括:

    读锁获取流程:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 graph LR
    2 A[开始尝试获取读锁] --> B{检查锁状态:无写锁且无写等待者?};
    3 B -- --> C{原子增加读锁计数};
    4 C -- 成功 --> D[获取读锁成功];
    5 C -- 失败 --> E[进入读等待队列,休眠];
    6 B -- --> E;
    7 E --> F[等待锁释放通知];
    8 F --> B;

    读锁释放流程:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 graph LR
    2 A[开始释放读锁] --> B{原子减少读锁计数};
    3 B --> C{读锁计数变为 0};
    4 C -- --> D{检查是否有等待写锁线程};
    5 D -- --> E[唤醒等待写锁线程];
    6 D -- --> F[结束];
    7 C -- --> F;

    写锁获取流程:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 graph LR
    2 A[开始尝试获取写锁] --> B{检查锁状态:无读锁且无写锁?};
    3 B -- --> C{原子设置为写锁状态};
    4 C -- 成功 --> D[获取写锁成功];
    5 C -- 失败 --> E[进入写等待队列,休眠];
    6 B -- --> E;
    7 E --> F[等待锁释放通知];
    8 F --> B;

    写锁释放流程:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 graph LR
    2 A[开始释放写锁] --> B{原子重置锁状态为 0};
    3 B --> C{检查是否有等待线程};
    4 C -- --> D[根据策略唤醒等待线程(优先写线程?)];
    5 D --> E[结束];
    6 C -- --> E;

    理解这些流程对于编写正确的并发程序至关重要。尤其需要注意原子操作的使用,以及等待队列和唤醒机制的配合,这些都是保证 SharedMutex 线程安全和高效性的关键。

    2.3 性能分析:读写性能与竞争 (Performance Analysis: Read/Write Performance and Contention)

    SharedMutex 的设计目标之一是优化读多写少的并发场景下的性能。与传统的互斥锁(std::mutex)相比,SharedMutex 允许多个线程同时持有读锁,从而显著提高了并发读取的性能。然而,在不同的工作负载和竞争条件下,SharedMutex 的性能表现也会有所不同。本节将分析 SharedMutex 在不同场景下的性能特点,以及竞争对性能的影响。

    ① 读多写少场景 (Read-Heavy Workloads)

    在读多写少的场景下,SharedMutex 能够充分发挥其优势。由于允许多个线程并发持有读锁,因此可以显著提高读取操作的吞吐量。

    高并发读取 (High Concurrent Reads):当多个线程同时尝试读取共享资源时,它们可以并发地获取读锁,并行执行读取操作,从而大大减少了等待时间,提高了系统的并发性能。
    低竞争 (Low Contention):在读操作为主的场景下,写操作相对较少,因此读线程之间的竞争非常低。读线程通常可以快速获取读锁,不会发生严重的阻塞。
    性能接近无锁 (Performance Close to Lock-Free):在理想的读多写少场景下,SharedMutex 的性能可以接近无锁的性能,因为读操作几乎可以并行执行,只有少量的写操作会引入锁竞争。

    ② 写多读少场景 (Write-Heavy Workloads)

    在写多读少的场景下,SharedMutex 的性能优势会降低,甚至可能退化到与普通互斥锁相近的水平。

    写操作串行化 (Serialized Write Operations):由于写锁是排他锁,当有线程持有写锁时,其他所有线程(包括读线程和写线程)都必须等待。因此,写操作本质上是串行执行的,无法并发。
    读操作受阻 (Blocked Read Operations):当有线程持有写锁时,后续的读线程也无法获取读锁,必须等待写锁释放。这会导致读操作被阻塞,降低了读取的并发性。
    高竞争 (High Contention):在写操作频繁的场景下,写锁的竞争会非常激烈。线程在尝试获取写锁时,可能会频繁地进入等待队列和被唤醒,导致较高的上下文切换开销。

    ③ 读写均衡场景 (Balanced Read-Write Workloads)

    在读写操作比例相对均衡的场景下,SharedMutex 的性能表现介于读多写少和写多读少之间。

    读写并发受限 (Limited Read-Write Concurrency):虽然 SharedMutex 允许并发读,但在读写混合的场景下,写操作会限制读操作的并发性。写操作的频率越高,读操作的并发性受限越严重。
    竞争程度适中 (Moderate Contention):读写均衡场景下的锁竞争程度取决于具体的读写比例和访问模式。如果读写操作交错频繁,则竞争会比较激烈;如果读写操作相对独立,则竞争会相对缓和。

    ④ 竞争对性能的影响 (Impact of Contention on Performance)

    锁竞争是影响 SharedMutex 性能的关键因素。当多个线程同时尝试获取锁时,会发生竞争,导致线程阻塞、上下文切换和延迟。

    上下文切换开销 (Context Switching Overhead):当线程因锁竞争而进入等待队列时,操作系统需要进行上下文切换,保存当前线程的状态,并切换到另一个线程执行。上下文切换本身会消耗 CPU 时间,降低系统性能。
    延迟增加 (Increased Latency):锁竞争会导致线程等待锁释放,从而增加了操作的延迟。在高竞争场景下,线程可能需要等待较长时间才能获取到锁,导致响应时间变长。
    公平性问题 (Fairness Issues):在高竞争场景下,可能会出现公平性问题,例如写饥饿。如果读操作非常频繁,写线程可能长时间无法获取到写锁,导致写操作被延迟。SharedMutex 的具体实现可能会采用一些策略来缓解公平性问题,例如写优先策略。

    ⑤ 性能优化技巧 (Performance Optimization Techniques)

    为了提高 SharedMutex 的性能,可以采取以下一些优化技巧:

    减少锁的持有时间 (Reduce Lock Holding Time):尽可能缩短临界区的代码执行时间,减少锁的持有时间,从而降低锁竞争的概率。
    细粒度锁 (Fine-Grained Locking):如果可能,将一个大的共享资源拆分成多个小的共享资源,并对每个小资源使用独立的 SharedMutex 进行保护。细粒度锁可以提高并发性,减少锁竞争。
    读写分离 (Read-Write Separation):在某些场景下,可以将读操作和写操作分离到不同的数据结构上,例如使用 Copy-on-Write 技术。读操作访问只读数据结构,无需加锁;写操作在修改数据时,先复制一份数据,修改副本,然后原子地替换旧数据。读写分离可以最大程度地提高读操作的并发性。
    选择合适的锁类型 (Choose the Right Lock Type):根据具体的应用场景选择合适的锁类型。如果读操作远多于写操作,SharedMutex 是一个很好的选择。如果读写操作比例均衡,或者写操作非常频繁,可能需要考虑其他并发控制机制,例如原子操作、无锁数据结构等。
    监控和调优 (Monitoring and Tuning):对 SharedMutex 的性能进行监控,例如监控锁的竞争程度、等待时间等。根据监控结果,调整锁的使用策略和参数,进行性能调优。

    理解 SharedMutex 的性能特点和竞争对性能的影响,有助于我们在实际应用中合理地选择和使用 SharedMutex,并采取相应的优化措施,以获得最佳的并发性能。

    2.4 SharedMutex 与 std::shared_mutex 对比 (Comparison between SharedMutex and std::shared_mutex)

    folly::SharedMutexstd::shared_mutex 都是 C++ 中提供的共享互斥锁,用于实现读写锁模式。虽然它们的目标相同,但在实现细节、性能特点和适用场景上存在一些差异。本节将对 SharedMutexstd::shared_mutex 进行详细对比,帮助读者理解它们的异同,并在实际应用中做出合适的选择。

    ① 功能特性对比 (Feature Comparison)

    特性 (Feature)folly::SharedMutexstd::shared_mutex
    所属库 (Library)Facebook FollyC++ 标准库 (C++17 起)
    公平性 (Fairness)通常提供更强的公平性保证,避免写饥饿 (可配置)公平性保证较弱,可能出现写饥饿
    性能 (Performance)在高并发读场景下通常性能更优,尤其是在高竞争情况下在低竞争场景下性能接近,高竞争下性能可能稍逊
    API 扩展性 (API Extensibility)提供更多的扩展 API,例如 lock_shared_forlock_exclusivelyAPI 相对简洁,主要提供基本的读写锁操作
    平台依赖 (Platform Dependency)依赖 Folly 库,跨平台兼容性取决于 Folly 库的移植性标准库组件,跨平台兼容性更好
    错误处理 (Error Handling)异常安全,提供 RAII 风格的锁管理类 (SharedLockGuard, ExclusiveLockGuard)异常安全,提供 RAII 风格的锁管理类 (shared_lock, unique_lock)
    调试支持 (Debugging Support)Folly 库通常提供更丰富的调试和诊断工具标准库的调试支持相对基础

    ② 性能差异分析 (Performance Difference Analysis)

    高并发读场景 (High Concurrent Read Scenarios)folly::SharedMutex 通常在高并发读场景下表现更优。这主要是因为 folly::SharedMutex 在实现上进行了更多的优化,例如更高效的等待队列管理、自旋优化、以及针对多核架构的优化等。尤其是在高竞争情况下,folly::SharedMutex 的性能优势更加明显。
    低竞争场景 (Low Contention Scenarios):在低竞争场景下,std::shared_mutex 的性能与 folly::SharedMutex 接近。因为在低竞争情况下,锁的获取和释放操作都非常快速,性能瓶颈不在于锁本身,而可能在于临界区的代码执行效率。
    写操作性能 (Write Operation Performance):由于写锁是排他锁,无论使用 folly::SharedMutex 还是 std::shared_mutex,写操作的性能都受到串行化的限制。在写密集型场景下,两者的性能差异可能不大。
    公平性对性能的影响 (Impact of Fairness on Performance)folly::SharedMutex 通常提供更强的公平性保证,这可能会在一定程度上牺牲性能。为了避免写饥饿,folly::SharedMutex 可能会优先唤醒写线程,即使此时有更多的读线程在等待。而 std::shared_mutex 的公平性较弱,可能导致写线程长时间等待,但理论上可能获得更高的吞吐量(但公平性差)。

    ③ 适用场景选择 (Scenario-Based Selection)

    优先选择 folly::SharedMutex 的场景 (Scenarios Favoring folly::SharedMutex)
    ▮▮▮▮⚝ 高并发、读多写少 的应用场景,例如缓存系统、高并发数据读取服务等。
    ▮▮▮▮⚝ 对性能要求非常高 的场景,需要尽可能地压榨系统性能。
    ▮▮▮▮⚝ 需要更强的公平性保证 的场景,例如需要避免写饥饿的应用。
    ▮▮▮▮⚝ 已经在使用 Folly 库 的项目,引入 folly::SharedMutex 的成本较低。

    优先选择 std::shared_mutex 的场景 (Scenarios Favoring std::shared_mutex)
    ▮▮▮▮⚝ 对性能要求不是极致,但需要读写锁功能的应用。
    ▮▮▮▮⚝ 对跨平台兼容性要求高 的项目,std::shared_mutex 是标准库组件,兼容性更好。
    ▮▮▮▮⚝ 项目依赖较少,避免引入额外的 Folly 库依赖。
    ▮▮▮▮⚝ C++17 或更高版本 的项目,可以直接使用 std::shared_mutex,无需额外引入库。
    ▮▮▮▮⚝ API 需求简单std::shared_mutex 的基本 API 已经足够满足需求。

    ④ 代码示例对比 (Code Example Comparison)

    下面分别使用 folly::SharedMutexstd::shared_mutex 实现一个简单的读写锁示例:

    使用 folly::SharedMutex:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <iostream>
    3 #include <thread>
    4 #include <vector>
    5
    6 folly::SharedMutex mutex;
    7 int shared_data = 0;
    8
    9 void read_data(int thread_id) {
    10 folly::SharedLockGuard lock(mutex); // 获取读锁
    11 std::cout << "Reader " << thread_id << ": Data = " << shared_data << std::endl;
    12 }
    13
    14 void write_data(int thread_id, int value) {
    15 folly::ExclusiveLockGuard lock(mutex); // 获取写锁
    16 shared_data = value;
    17 std::cout << "Writer " << thread_id << ": Updated data to " << value << std::endl;
    18 }
    19
    20 int main() {
    21 std::vector<std::thread> threads;
    22 for (int i = 0; i < 5; ++i) {
    23 threads.emplace_back(read_data, i);
    24 }
    25 threads.emplace_back(write_data, 0, 100);
    26 for (int i = 5; i < 10; ++i) {
    27 threads.emplace_back(read_data, i);
    28 }
    29
    30 for (auto& thread : threads) {
    31 thread.join();
    32 }
    33 return 0;
    34 }

    使用 std::shared_mutex:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <shared_mutex>
    2 #include <iostream>
    3 #include <thread>
    4 #include <vector>
    5
    6 std::shared_mutex mutex;
    7 int shared_data = 0;
    8
    9 void read_data(int thread_id) {
    10 std::shared_lock<std::shared_mutex> lock(mutex); // 获取读锁
    11 std::cout << "Reader " << thread_id << ": Data = " << shared_data << std::endl;
    12 }
    13
    14 void write_data(int thread_id, int value) {
    15 std::unique_lock<std::shared_mutex> lock(mutex); // 获取写锁
    16 shared_data = value;
    17 std::cout << "Writer " << thread_id << ": Updated data to " << value << std::endl;
    18 }
    19
    20 int main() {
    21 std::vector<std::thread> threads;
    22 for (int i = 0; i < 5; ++i) {
    23 threads.emplace_back(read_data, i);
    24 }
    25 threads.emplace_back(write_data, 0, 100);
    26 for (int i = 5; i < 10; ++i) {
    27 threads.emplace_back(read_data, i);
    28 }
    29
    30 for (auto& thread : threads) {
    31 thread.join();
    32 }
    33 return 0;
    34 }

    从代码示例可以看出,两者的 API 使用方式非常相似,都提供了 RAII 风格的锁管理类 (SharedLockGuard/ExclusiveLockGuard vs. shared_lock/unique_lock),简化了锁的获取和释放操作,并保证了异常安全性。

    总结 (Summary)

    folly::SharedMutexstd::shared_mutex 都是优秀的共享互斥锁实现,选择哪一个取决于具体的应用场景和需求。如果追求极致的性能,尤其是在高并发读场景下,并且项目已经使用了 Folly 库,或者可以接受引入 Folly 库的依赖,那么 folly::SharedMutex 是一个更好的选择。如果更看重跨平台兼容性、标准库依赖,并且对性能要求不是特别苛刻,那么 std::shared_mutex 也是一个不错的选择。在实际开发中,可以根据项目的具体情况,权衡各种因素,选择最合适的共享互斥锁。

    END_OF_CHAPTER

    3. chapter 3: SharedMutex 高级应用 (Advanced Applications of SharedMutex)

    3.1 超时锁与非阻塞锁 (Timeout Locks and Non-blocking Locks)

    在并发编程中,锁机制是保证数据一致性和线程安全的关键工具。然而,在某些场景下,无限制地等待锁释放可能会导致程序响应性降低,甚至出现死锁。为了应对这些问题,folly::SharedMutex 提供了超时锁(Timeout Locks)非阻塞锁(Non-blocking Locks) 的功能,允许线程在尝试获取锁时设置超时时间或立即返回,从而提高了程序的灵活性和健壮性。

    3.1.1 超时锁 (Timeout Locks)

    超时锁 允许线程在尝试获取锁时指定一个最大等待时间。如果在指定时间内成功获取到锁,线程可以继续执行;如果超时时间到达仍未获取到锁,则线程会放弃等待并返回一个表示失败的信号,例如 false

    folly::SharedMutex 提供了带有超时功能的 try_lock_fortry_lock_shared_for 方法,它们接受一个时间段 std::chrono::duration 作为参数,表示最大等待时间。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <chrono>
    3 #include <thread>
    4 #include <iostream>
    5
    6 folly::SharedMutex mutex;
    7 int shared_data = 0;
    8
    9 void writer_timeout() {
    10 std::cout << "Writer (timeout) thread trying to acquire exclusive lock with timeout..." << std::endl;
    11 if (mutex.try_lock_for(std::chrono::seconds(2))) {
    12 std::cout << "Writer (timeout) thread acquired exclusive lock." << std::endl;
    13 shared_data++; // 访问共享资源
    14 std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟写操作
    15 mutex.unlock();
    16 std::cout << "Writer (timeout) thread released exclusive lock." << std::endl;
    17 } else {
    18 std::cout << "Writer (timeout) thread failed to acquire exclusive lock within timeout." << std::endl;
    19 }
    20 }
    21
    22 void reader_timeout() {
    23 std::cout << "Reader (timeout) thread trying to acquire shared lock with timeout..." << std::endl;
    24 if (mutex.try_lock_shared_for(std::chrono::milliseconds(500))) {
    25 std::cout << "Reader (timeout) thread acquired shared lock." << std::endl;
    26 std::cout << "Shared data: " << shared_data << std::endl; // 读取共享资源
    27 std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 模拟读操作
    28 mutex.unlock_shared();
    29 std::cout << "Reader (timeout) thread released shared lock." << std::endl;
    30 } else {
    31 std::cout << "Reader (timeout) thread failed to acquire shared lock within timeout." << std::endl;
    32 }
    33 }
    34
    35 int main() {
    36 std::thread writer_thread(writer_timeout);
    37 std::thread reader_thread(reader_timeout);
    38
    39 writer_thread.join();
    40 reader_thread.join();
    41
    42 return 0;
    43 }

    代码解释:

    writer_timeout 函数尝试在 2 秒内获取排他锁(exclusive lock)。如果成功,则更新 shared_data 并释放锁;如果超时,则输出获取锁失败的信息。
    reader_timeout 函数尝试在 500 毫秒内获取共享锁(shared lock)。如果成功,则读取 shared_data 并释放锁;如果超时,则输出获取锁失败的信息。

    应用场景:

    防止无限等待: 当线程在等待锁时,可能由于各种原因(例如,持有锁的线程崩溃或进入死循环)导致无限期等待。超时锁可以避免这种情况,保证程序不会被永久阻塞。
    资源竞争激烈: 在高并发系统中,锁的竞争可能非常激烈。使用超时锁可以使线程在等待一段时间后放弃竞争,从而尝试执行其他任务或稍后重试,提高系统的整体吞吐量。
    服务降级: 在某些关键业务场景下,如果获取锁超时,可以采取服务降级策略,例如返回缓存数据或默认值,而不是无限期等待最新的数据,保证服务的可用性。

    3.1.2 非阻塞锁 (Non-blocking Locks)

    非阻塞锁 是一种尝试立即获取锁的机制,无论锁是否可用,都会立即返回。如果锁当前可用,则成功获取并继续执行;如果锁当前被其他线程持有,则立即返回失败,而不会进入等待状态。

    folly::SharedMutex 提供了 try_locktry_lock_shared 方法来实现非阻塞锁。这些方法会立即返回,并返回一个 bool 值表示是否成功获取到锁。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <thread>
    3 #include <iostream>
    4
    5 folly::SharedMutex mutex_non_blocking;
    6 int shared_data_non_blocking = 0;
    7
    8 void writer_non_blocking() {
    9 std::cout << "Writer (non-blocking) thread trying to acquire exclusive lock..." << std::endl;
    10 if (mutex_non_blocking.try_lock()) {
    11 std::cout << "Writer (non-blocking) thread acquired exclusive lock." << std::endl;
    12 shared_data_non_blocking++; // 访问共享资源
    13 std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟写操作
    14 mutex_non_blocking.unlock();
    15 std::cout << "Writer (non-blocking) thread released exclusive lock." << std::endl;
    16 } else {
    17 std::cout << "Writer (non-blocking) thread failed to acquire exclusive lock immediately." << std::endl;
    18 }
    19 }
    20
    21 void reader_non_blocking() {
    22 std::cout << "Reader (non-blocking) thread trying to acquire shared lock..." << std::endl;
    23 if (mutex_non_blocking.try_lock_shared()) {
    24 std::cout << "Reader (non-blocking) thread acquired shared lock." << std::endl;
    25 std::cout << "Shared data: " << shared_data_non_blocking << std::endl; // 读取共享资源
    26 std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 模拟读操作
    27 mutex_non_blocking.unlock_shared();
    28 std::cout << "Reader (non-blocking) thread released shared lock." << std::endl;
    29 } else {
    30 std::cout << "Reader (non-blocking) thread failed to acquire shared lock immediately." << std::endl;
    31 }
    32 }
    33
    34 int main() {
    35 std::thread writer_thread_nb(writer_non_blocking);
    36 std::thread reader_thread_nb(reader_non_blocking);
    37
    38 writer_thread_nb.join();
    39 reader_thread_nb.join();
    40
    41 return 0;
    42 }

    代码解释:

    writer_non_blocking 函数尝试立即获取排他锁。如果成功,则更新 shared_data_non_blocking 并释放锁;如果立即获取失败,则输出获取锁失败的信息。
    reader_non_blocking 函数尝试立即获取共享锁。如果成功,则读取 shared_data_non_blocking 并释放锁;如果立即获取失败,则输出获取锁失败的信息。

    应用场景:

    轮询与重试机制: 非阻塞锁常用于实现轮询或重试机制。当线程尝试获取锁失败时,可以立即返回并执行其他任务,或者稍后再次尝试获取锁,而不会一直阻塞等待。
    避免优先级反转: 在某些实时系统中,高优先级线程如果因为等待低优先级线程持有的锁而被阻塞,可能会导致优先级反转(priority inversion) 问题。使用非阻塞锁,高优先级线程可以立即检测锁是否可用,如果不可用,则可以先执行其他任务,避免被低优先级线程阻塞。
    状态检查: 非阻塞锁可以用于快速检查某个资源是否被占用。例如,在资源调度系统中,可以使用非阻塞锁来检查某个资源是否空闲,从而决定是否将任务调度到该资源上。

    3.2 SharedMutex 与条件变量 (SharedMutex and Condition Variables)

    条件变量(Condition Variable) 是一种同步原语,允许线程在满足特定条件时才被唤醒。它通常与互斥锁一起使用,用于实现更复杂的线程同步和通信。folly::SharedMutex 也可以与条件变量结合使用,以实现基于读写锁的条件等待和通知机制。

    folly::SharedMutex 本身不直接提供条件变量的功能。但是,我们可以结合 std::condition_variablefolly::Baton 等条件变量实现,来扩展 SharedMutex 的应用场景。

    以下示例展示了如何使用 std::condition_variablefolly::SharedMutex 实现一个简单的生产者-消费者模型,其中多个消费者可以同时读取数据,但生产者在写入数据时需要独占访问。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <condition_variable>
    3 #include <mutex>
    4 #include <thread>
    5 #include <vector>
    6 #include <iostream>
    7
    8 folly::SharedMutex mutex_cv;
    9 std::condition_variable cv;
    10 std::mutex std_mutex; // 用于 condition_variable 的 std::mutex
    11 std::vector<int> data_buffer;
    12 bool data_ready = false;
    13
    14 void producer_cv() {
    15 for (int i = 0; i < 5; ++i) {
    16 std::this_thread::sleep_for(std::chrono::seconds(1));
    17 {
    18 std::lock_guard<folly::SharedMutex> lock(mutex_cv); // 获取排他锁
    19 data_buffer.push_back(i);
    20 data_ready = true;
    21 std::cout << "Producer produced data: " << i << std::endl;
    22 }
    23 cv.notify_all(); // 通知所有消费者数据已准备好
    24 }
    25 }
    26
    27 void consumer_cv(int id) {
    28 while (true) {
    29 {
    30 std::unique_lock<std::mutex> lock_std(std_mutex); // condition_variable 需要 std::mutex
    31 cv.wait(lock_std, []{ return data_ready; }); // 等待条件变量
    32
    33 {
    34 std::shared_lock<folly::SharedMutex> shared_lock(mutex_cv); // 获取共享锁
    35 if (data_buffer.empty()) {
    36 data_ready = false; // 数据被消费完,重置 data_ready
    37 break; // 生产者生产完成,消费者也退出
    38 }
    39 int data = data_buffer.back();
    40 data_buffer.pop_back();
    41 std::cout << "Consumer " << id << " consumed data: " << data << std::endl;
    42 }
    43 }
    44 }
    45 }
    46
    47 int main() {
    48 std::thread producer_thread_cv(producer_cv);
    49 std::vector<std::thread> consumer_threads_cv;
    50 for (int i = 0; i < 3; ++i) {
    51 consumer_threads_cv.emplace_back(consumer_cv, i);
    52 }
    53
    54 producer_thread_cv.join();
    55 for (auto& thread : consumer_threads_cv) {
    56 thread.join();
    57 }
    58
    59 return 0;
    60 }

    代码解释:

    生产者 (producer_cv):
    ▮▮▮▮⚝ 循环生产数据,每次生产数据前休眠 1 秒。
    ▮▮▮▮⚝ 使用 std::lock_guard<folly::SharedMutex> 获取 排他锁,保证在写入数据时没有其他线程可以访问 data_buffer
    ▮▮▮▮⚝ 将数据添加到 data_buffer,设置 data_readytrue,表示数据已准备好。
    ▮▮▮▮⚝ 调用 cv.notify_all() 通知所有等待的消费者线程。
    消费者 (consumer_cv):
    ▮▮▮▮⚝ 使用 std::unique_lock<std::mutex>cv.wait() 等待条件变量 cv,当 data_readytrue 时被唤醒。 注意: std::condition_variable 必须与 std::mutex 配合使用。
    ▮▮▮▮⚝ 唤醒后,使用 std::shared_lock<folly::SharedMutex> 获取 共享锁,允许多个消费者同时读取 data_buffer
    ▮▮▮▮⚝ 从 data_buffer 中取出数据并消费。
    ▮▮▮▮⚝ 如果 data_buffer 为空,则设置 data_readyfalse,并退出循环(假设生产者生产完所有数据后不再生产)。

    关键点:

    条件等待: 消费者线程使用 cv.wait() 进入等待状态,直到生产者线程调用 cv.notify_all() 唤醒。
    读写分离: 生产者使用 排他锁 写入数据,消费者使用 共享锁 读取数据,允许多个消费者并发读取,提高了并发性能。
    std::mutex 的必要性: std::condition_variablewait()notify_all() 方法必须在与同一个 std::mutex 关联的锁的保护下调用。这是 std::condition_variable 的规范要求。

    folly::Baton 替代 std::condition_variable:

    folly 库也提供了 folly::Baton,它是一个更轻量级、更易于使用的同步原语,可以替代 std::condition_variable。使用 folly::Baton 可以简化代码,并可能带来更好的性能。 将上述代码中的 std::condition_variablestd::mutex 替换为 folly::Baton,代码会更加简洁。 (此处省略 folly::Baton 的代码示例,读者可以自行尝试替换和实现)。

    3.3 实战案例:高并发缓存系统 (Practical Case: High-Concurrency Cache System)

    缓存系统是现代互联网应用中不可或缺的组件,用于加速数据访问,降低数据库负载。在高并发场景下,如何设计一个高效、线程安全的缓存系统至关重要。folly::SharedMutex 非常适合用于构建高并发缓存系统,因为它允许并发读取缓存数据,同时保证独占写入缓存数据的一致性。

    缓存系统设计要点:

    数据存储: 使用 std::unordered_mapfolly::ConcurrentHashMap 等高效的哈希表来存储缓存数据,键为缓存 Key,值为缓存 Value。
    缓存项(Cache Item): 每个缓存项需要包含数据 Value 和一个 folly::SharedMutex 用于保护该缓存项的并发访问。
    缓存操作:
    ▮▮▮▮⚝ 读取缓存 (Cache Get): 尝试获取缓存项的 共享锁。如果成功获取,则读取缓存数据并返回;如果缓存未命中或获取锁失败(例如,正在被写入),则返回未命中。
    ▮▮▮▮⚝ 写入缓存 (Cache Set): 获取缓存项的 排他锁。如果成功获取,则更新缓存数据并释放锁。
    ▮▮▮▮⚝ 删除缓存 (Cache Delete): 获取缓存项的 排他锁。如果成功获取,则删除缓存数据并释放锁。

    简化版代码示例 (伪代码,仅展示核心逻辑):

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <unordered_map>
    3 #include <string>
    4 #include <memory>
    5
    6 class CacheItem {
    7 public:
    8 CacheItem(std::string value) : value_(std::move(value)) {}
    9 std::string value_;
    10 folly::SharedMutex mutex_; // 每个缓存项一个 SharedMutex
    11 };
    12
    13 class ConcurrentCache {
    14 public:
    15 using KeyType = std::string;
    16 using ValueType = std::string;
    17
    18 std::shared_ptr<CacheItem> get(const KeyType& key) {
    19 auto it = cache_data_.find(key);
    20 if (it != cache_data_.end()) {
    21 std::shared_lock<folly::SharedMutex> lock(it->second->mutex_); // 获取共享锁
    22 return it->second; // 返回缓存项 (智能指针)
    23 }
    24 return nullptr; // 缓存未命中
    25 }
    26
    27 void set(const KeyType& key, const ValueType& value) {
    28 std::unique_lock<folly::SharedMutex> lock(get_item_mutex(key)); // 获取 item_mutex 的排他锁
    29 auto item_ptr = std::make_shared<CacheItem>(value);
    30 cache_data_[key] = item_ptr;
    31 }
    32
    33 void remove(const KeyType& key) {
    34 std::unique_lock<folly::SharedMutex> lock(get_item_mutex(key)); // 获取 item_mutex 的排他锁
    35 cache_data_.erase(key);
    36 }
    37
    38 private:
    39 std::unordered_map<KeyType, std::shared_ptr<CacheItem>> cache_data_;
    40 folly::SharedMutex item_mutex_map_; // 保护 cache_data_ 的互斥锁 (可选,更细粒度锁可以提升并发)
    41
    42 // 更细粒度的锁,针对每个 key 的互斥锁,避免修改 cache_data_ 时的全局锁竞争
    43 folly::SharedMutex& get_item_mutex(const KeyType& key) {
    44 // 实际应用中,需要更高效的 key 到 mutex 的映射,例如使用 ConcurrentHashMap<KeyType, SharedMutex>
    45 // 这里为了简化示例,假设使用全局的 item_mutex_map_ 保护 cache_data_ 的修改
    46 return item_mutex_map_;
    47 }
    48 };

    代码解释 (简化版):

    CacheItem: 封装缓存值 value_ 和用于保护该缓存项的 mutex_
    ConcurrentCache:
    ▮▮▮▮⚝ cache_data_: 使用 std::unordered_map 存储缓存数据,Key 为 KeyType,Value 为指向 CacheItem 的智能指针 std::shared_ptr<CacheItem>
    ▮▮▮▮⚝ get(key): 尝试获取缓存项 mutex_共享锁,成功则返回缓存项的智能指针,否则返回 nullptr (缓存未命中)。
    ▮▮▮▮⚝ set(key, value): 获取 item_mutex_map_排他锁 (实际应用中,可以更细粒度地管理每个 key 的互斥锁),创建新的 CacheItem 并更新 cache_data_
    ▮▮▮▮⚝ remove(key): 获取 item_mutex_map_排他锁,从 cache_data_ 中删除缓存项。
    ▮▮▮▮⚝ item_mutex_map_: 可选的,用于保护 cache_data_ 的修改操作。在更细粒度的锁设计中,可以针对每个 key 维护一个 SharedMutex,进一步提升并发性能。

    高并发优化:

    细粒度锁: 示例中使用 item_mutex_map_ 保护 cache_data_ 的修改操作,但更优的设计是为每个缓存 Key 关联一个独立的 folly::SharedMutex,这样可以最大程度地减少锁竞争,提高并发性能。可以使用 folly::ConcurrentHashMap<KeyType, folly::SharedMutex> 来管理 Key 到 SharedMutex 的映射。
    读写分离: SharedMutex 允许并发读取,极大地提高了缓存的读取性能。
    缓存淘汰策略: 缓存系统通常需要实现缓存淘汰策略(例如 LRU, FIFO)来限制缓存大小,并淘汰不常用的缓存项。缓存淘汰策略的实现也需要考虑并发安全性,可以使用 SharedMutex 或其他并发控制机制来保护缓存元数据的更新。

    3.4 设计模式:读写锁模式的应用 (Design Pattern: Application of Reader-Writer Lock Pattern)

    读写锁模式(Reader-Writer Lock Pattern) 是一种常用的并发设计模式,用于优化对共享资源的并发访问。它允许多个读者(Reader) 同时访问共享资源,但只允许一个写者(Writer) 独占访问共享资源。这种模式非常适合读多写少的场景,可以显著提高系统的并发性能。

    读写锁模式的核心思想:

    共享锁 (Shared Lock / Read Lock): 多个读者可以同时持有共享锁。当持有共享锁时,可以读取共享资源,但不能修改。
    排他锁 (Exclusive Lock / Write Lock): 只允许一个写者持有排他锁。当持有排他锁时,可以读取和修改共享资源,并且任何读者或写者都不能同时持有锁。

    folly::SharedMutex 正是读写锁模式的实现

    lock_shared() / try_lock_shared() / unlock_shared(): 用于获取和释放 共享锁,允许多个读者并发访问。
    lock() / try_lock() / unlock(): 用于获取和释放 排他锁,保证写者的独占访问。

    读写锁模式的应用场景:

    缓存系统: 如 3.3 节所述,缓存系统是读写锁模式的典型应用场景。
    文件系统: 多个进程可以同时读取同一个文件,但只有一个进程可以写入文件。
    数据库系统: 数据库的读操作通常远多于写操作。读写锁模式可以提高数据库的并发读取性能。
    配置中心: 多个服务实例可以同时读取配置信息,但只有管理员可以修改配置。
    地理信息系统 (GIS): 多个用户可以同时查询地图数据,但只有数据维护人员可以更新地图数据。

    读写锁模式的优点:

    提高并发性: 允许多个读者并发访问,提高了系统的并发性能,尤其是在读多写少的场景下。
    降低锁竞争: 读操作之间不会互斥,减少了锁的竞争,提高了吞吐量。
    提升响应速度: 读操作可以快速获取共享锁并执行,降低了读操作的延迟。

    读写锁模式的缺点:

    写饥饿 (Writer Starvation): 如果读操作非常频繁,写操作可能会长时间等待,导致写饥饿。可以通过写者优先策略来缓解写饥饿问题,但 folly::SharedMutex 默认是读者优先的。
    实现复杂性: 读写锁的实现比简单的互斥锁要复杂,需要更仔细地考虑锁的获取和释放逻辑。
    适用场景限制: 读写锁模式只适用于读多写少的场景。如果读写操作的比例接近或写操作更多,读写锁模式的性能优势可能不明显,甚至可能不如简单的互斥锁。

    总结:

    读写锁模式是一种强大的并发设计模式,folly::SharedMutex 提供了对读写锁模式的有效实现。在设计高并发系统时,合理地应用读写锁模式,可以显著提高系统的并发性能和响应速度。但需要注意读写锁模式的适用场景和潜在的写饥饿问题,并根据实际情况选择合适的并发控制策略。

    END_OF_CHAPTER

    4. chapter 4: SharedMutex 最佳实践与避坑指南 (Best Practices and Pitfalls of SharedMutex)

    4.1 高效使用 SharedMutex 的技巧 (Tips for Efficiently Using SharedMutex)

    高效地使用 folly::SharedMutex 可以显著提升并发程序的性能和可维护性。本节将深入探讨一些关键技巧,帮助读者充分发挥 SharedMutex 的优势,并避免常见的性能瓶颈。

    4.1.1 细粒度锁与粗粒度锁的权衡 (Granular vs. Coarse-grained Locking)

    在并发编程中,锁的粒度是一个至关重要的设计决策。它直接影响到程序的并发度和性能。

    细粒度锁 (Fine-grained Locking)
    指将锁的保护范围缩小,尽可能只保护真正需要互斥访问的共享资源。
    ▮▮▮▮ⓐ 优点
    ▮▮▮▮▮▮▮▮❷ 提高并发度: 允许多个线程同时访问不同的共享资源,减少线程间的竞争,提升系统吞吐量。
    ▮▮▮▮▮▮▮▮❸ 减少锁冲突: 降低了锁冲突的可能性,减少线程阻塞和上下文切换的开销。
    ▮▮▮▮ⓓ 缺点
    ▮▮▮▮▮▮▮▮❺ 增加锁管理开销: 需要管理更多的锁对象,增加了代码的复杂性和维护成本。
    ▮▮▮▮▮▮▮▮❻ 可能引入死锁风险: 当需要同时获取多个锁时,如果锁的获取顺序不当,容易导致死锁。

    粗粒度锁 (Coarse-grained Locking)
    指使用较少的锁来保护较大范围的共享资源。
    ▮▮▮▮ⓐ 优点
    ▮▮▮▮▮▮▮▮❷ 简化锁管理: 减少了锁的数量,降低了代码的复杂性,易于理解和维护。
    ▮▮▮▮▮▮▮▮❸ 降低死锁风险: 由于锁的数量较少,减少了多锁竞争导致的死锁风险。
    ▮▮▮▮ⓓ 缺点
    ▮▮▮▮▮▮▮▮❺ 降低并发度: 多个线程即使访问不同的子资源,也可能因为竞争同一个锁而串行化,降低了系统并发性能。
    ▮▮▮▮▮▮▮▮❻ 增加锁冲突: 锁的竞争程度增加,可能导致线程频繁阻塞和上下文切换,降低性能。

    权衡与选择
    选择细粒度锁还是粗粒度锁,需要根据具体的应用场景和共享资源的访问模式进行权衡。
    读多写少场景: 倾向于使用细粒度锁,并结合 SharedMutex 的共享锁特性,允许多个读者并发访问,仅在写操作时使用排他锁。
    写多读少场景: 可以考虑适当增加锁的粒度,减少锁管理的开销,但仍需注意避免过度粗粒度导致并发度下降。
    复杂数据结构: 对于复杂的数据结构,例如哈希表或树,可以考虑对数据结构的不同部分(例如哈希桶、树节点)使用不同的锁,实现更细粒度的并发控制。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <vector>
    3 #include <thread>
    4 #include <iostream>
    5
    6 folly::SharedMutex mutex;
    7 std::vector<int> data;
    8
    9 void reader() {
    10 for (int i = 0; i < 5; ++i) {
    11 mutex.lock_shared(); // 获取共享锁
    12 std::cout << "Reader: Data size = " << data.size() << std::endl;
    13 mutex.unlock_shared(); // 释放共享锁
    14 std::this_thread::sleep_for(std::chrono::milliseconds(100));
    15 }
    16 }
    17
    18 void writer() {
    19 for (int i = 0; i < 3; ++i) {
    20 mutex.lock(); // 获取排他锁
    21 data.push_back(i);
    22 std::cout << "Writer: Added data, size = " << data.size() << std::endl;
    23 mutex.unlock(); // 释放排他锁
    24 std::this_thread::sleep_for(std::chrono::milliseconds(200));
    25 }
    26 }
    27
    28 int main() {
    29 std::thread reader1(reader);
    30 std::thread reader2(reader);
    31 std::thread writer1(writer);
    32
    33 reader1.join();
    34 reader2.join();
    35 writer1.join();
    36
    37 return 0;
    38 }

    代码解释
    上述代码示例展示了读写锁的基本用法。多个 reader 线程可以同时获取共享锁读取 data,而 writer 线程在获取排他锁写入 data 时,会阻塞其他 reader 和 writer 线程。通过使用 SharedMutex,实现了读操作的并发执行,提高了程序的并发性能。

    4.1.2 最小化锁持有时间 (Minimize Lock Holding Time)

    减少锁的持有时间是提高并发程序性能的关键策略之一。锁在被持有的期间,会阻塞其他线程对共享资源的访问,持有时间越长,并发度越低。

    只保护必要的代码段
    将锁的保护范围限制在真正需要互斥访问共享资源的代码段。避免将与共享资源无关的代码(例如,耗时的计算、I/O 操作)放在锁的保护范围内。

    减少临界区代码量
    优化临界区(critical section)的代码逻辑,尽可能减少临界区代码的执行时间。例如,可以将一些可以在锁外完成的计算或数据准备工作移到临界区之外。

    使用本地变量
    在临界区内,尽量使用本地变量进行数据操作,减少对共享资源的直接访问次数。可以将共享数据复制到本地变量,在本地变量上进行操作,操作完成后再将结果写回共享资源。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <iostream>
    3 #include <string>
    4
    5 folly::SharedMutex mutex;
    6 std::string shared_string;
    7
    8 std::string process_string() {
    9 std::string local_string;
    10 {
    11 std::lock_guard<folly::SharedMutex> lock(mutex); // RAII 自动管理锁
    12 local_string = shared_string; // 快速复制共享数据到本地变量
    13 } // 锁在作用域结束时自动释放
    14
    15 // 在锁外进行耗时操作
    16 std::string result = local_string + " processed";
    17 return result;
    18 }
    19
    20 void update_string(const std::string& new_string) {
    21 std::lock_guard<folly::SharedMutex> lock(mutex);
    22 shared_string = new_string;
    23 }
    24
    25 int main() {
    26 update_string("Initial string");
    27 std::cout << process_string() << std::endl;
    28 return 0;
    29 }

    代码解释
    process_string 函数中,首先使用 std::lock_guard 获取锁,并将 shared_string 复制到本地变量 local_string,然后立即释放锁。耗时的字符串处理操作 local_string + " processed" 在锁外进行,从而减少了锁的持有时间,提高了并发性能。

    4.1.3 使用 RAII 管理锁 (Use RAII to Manage Locks)

    资源获取即初始化 (Resource Acquisition Is Initialization, RAII) 是一种 C++ 编程技术,它将资源的生命周期与对象的生命周期绑定在一起。对于锁的管理,RAII 技术可以确保锁的自动获取和释放,避免因忘记释放锁而导致的死锁或资源泄漏问题。

    std::lock_guard
    std::lock_guard 是一个 RAII 锁管理类,它在构造函数中获取锁,在析构函数中释放锁。std::lock_guard 适用于排他锁和共享锁,可以与 folly::SharedMutex 配合使用。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void exclusive_access() {
    2 std::lock_guard<folly::SharedMutex> lock(mutex); // 获取排他锁
    3 // 临界区代码
    4 // ...
    5 } // 作用域结束,lock 析构,自动释放排他锁
    6
    7 void shared_access() {
    8 std::shared_lock<folly::SharedMutex> lock(mutex); // 获取共享锁
    9 // 临界区代码
    10 // ...
    11 } // 作用域结束,lock 析构,自动释放共享锁

    std::unique_lockstd::shared_lock
    std::unique_lockstd::shared_lock 是更灵活的 RAII 锁管理类,它们提供了更多的控制选项,例如延迟锁定、尝试锁定、超时锁定等。std::unique_lock 用于管理排他锁,std::shared_lock 用于管理共享锁。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void try_exclusive_access() {
    2 std::unique_lock<folly::SharedMutex> lock(mutex, std::try_to_lock); // 尝试获取排他锁,非阻塞
    3 if (lock.owns_lock()) {
    4 // 获取锁成功,临界区代码
    5 // ...
    6 } else {
    7 // 获取锁失败,处理逻辑
    8 // ...
    9 }
    10 } // 作用域结束,lock 析构,如果持有锁则自动释放
    11
    12 void timed_shared_access() {
    13 std::shared_lock<folly::SharedMutex> lock(mutex, std::chrono::milliseconds(100)); // 超时等待获取共享锁
    14 if (lock.owns_lock()) {
    15 // 获取锁成功,临界区代码
    16 // ...
    17 } else {
    18 // 超时,处理逻辑
    19 // ...
    20 }
    21 } // 作用域结束,lock 析构,如果持有锁则自动释放

    自定义 RAII 锁管理类
    在某些复杂场景下,可以自定义 RAII 锁管理类,以满足特定的需求。例如,可以创建一个 RAII 类,在获取锁之前执行一些预处理操作,在释放锁之后执行一些清理操作。

    使用 RAII 管理锁可以显著提高代码的健壮性和可维护性,避免手动管理锁可能引入的错误。推荐在并发编程中始终使用 RAII 技术来管理 folly::SharedMutex

    4.1.4 避免在锁内执行耗时操作 (Avoid Long-Running Operations Inside Locks)

    在锁的保护范围内执行耗时操作会显著降低程序的并发性能。耗时操作会延长锁的持有时间,增加锁冲突的可能性,导致其他线程长时间阻塞等待。

    识别耗时操作
    识别临界区内的耗时操作,例如:
    I/O 操作: 文件读写、网络请求、数据库访问等 I/O 操作通常比较耗时。
    复杂计算: 复杂的数学计算、图像处理、视频编解码等计算密集型操作也可能比较耗时。
    内存分配: 频繁的内存分配和释放操作也可能成为性能瓶颈。

    将耗时操作移出临界区
    尽可能将耗时操作移出临界区,在锁外执行。例如,可以将需要写入文件的数据在锁内准备好,然后释放锁,在锁外执行文件写入操作。

    异步操作
    对于某些可以异步执行的耗时操作,可以考虑使用异步编程技术(例如,线程池、异步任务)将其移出临界区。在临界区内只进行必要的同步操作,然后将耗时操作提交给异步任务执行,待异步任务完成后再同步结果。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <fstream>
    3 #include <string>
    4 #include <iostream>
    5
    6 folly::SharedMutex mutex;
    7 std::string shared_data;
    8
    9 void write_data_to_file(const std::string& filename) {
    10 std::string data_to_write;
    11 {
    12 std::shared_lock<folly::SharedMutex> lock(mutex);
    13 data_to_write = shared_data; // 在锁内快速复制数据
    14 } // 释放锁
    15
    16 // 在锁外执行耗时的文件写入操作
    17 std::ofstream outfile(filename);
    18 outfile << data_to_write << std::endl;
    19 outfile.close();
    20 std::cout << "Data written to file: " << filename << std::endl;
    21 }
    22
    23 void update_shared_data(const std::string& new_data) {
    24 std::lock_guard<folly::SharedMutex> lock(mutex);
    25 shared_data = new_data;
    26 }
    27
    28 int main() {
    29 update_shared_data("This is some data to be written to file.");
    30 write_data_to_file("output.txt");
    31 return 0;
    32 }

    代码解释
    write_data_to_file 函数中,数据复制操作 data_to_write = shared_data 在锁内快速完成,然后立即释放锁。耗时的文件写入操作 std::ofstream outfile(filename) 在锁外执行,避免了在锁内执行耗时 I/O 操作,提高了程序的并发性能。

    4.1.5 读写分离策略 (Read-Write Separation Strategy)

    读写分离是一种常见的优化策略,特别适用于读多写少的场景。通过将读操作和写操作分离,可以最大程度地提高读操作的并发性。folly::SharedMutex 天然支持读写分离。

    使用共享锁保护读操作
    对于只读操作,使用共享锁 (lock_shared, shared_lock) 保护。多个线程可以同时持有共享锁,并发执行读操作,不会相互阻塞。

    使用排他锁保护写操作
    对于写操作,使用排他锁 (lock, lock_guard) 保护。当一个线程持有排他锁时,其他所有线程(包括读线程和写线程)都将被阻塞,确保数据的一致性和完整性。

    优化读路径
    在读多写少的场景下,应该重点优化读路径的性能。例如,可以使用缓存技术、数据预取等手段,减少读操作的延迟。

    控制写频率
    在某些场景下,可以适当控制写操作的频率,例如,采用批量写入、延迟写入等策略,减少写操作对读操作的影响。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <vector>
    3 #include <thread>
    4 #include <iostream>
    5
    6 folly::SharedMutex mutex;
    7 std::vector<int> data;
    8
    9 void reader_thread(int id) {
    10 for (int i = 0; i < 10; ++i) {
    11 {
    12 std::shared_lock<folly::SharedMutex> lock(mutex); // 获取共享锁
    13 std::cout << "Reader " << id << ": Data size = " << data.size() << std::endl;
    14 } // 释放共享锁
    15 std::this_thread::sleep_for(std::chrono::milliseconds(50));
    16 }
    17 }
    18
    19 void writer_thread(int id) {
    20 for (int i = 0; i < 5; ++i) {
    21 {
    22 std::lock_guard<folly::SharedMutex> lock(mutex); // 获取排他锁
    23 data.push_back(i * 100 + id);
    24 std::cout << "Writer " << id << ": Added data, size = " << data.size() << std::endl;
    25 } // 释放排他锁
    26 std::this_thread::sleep_for(std::chrono::milliseconds(100));
    27 }
    28 }
    29
    30 int main() {
    31 std::vector<std::thread> readers;
    32 for (int i = 0; i < 3; ++i) {
    33 readers.emplace_back(reader_thread, i);
    34 }
    35 std::vector<std::thread> writers;
    36 for (int i = 0; i < 2; ++i) {
    37 writers.emplace_back(writer_thread, i);
    38 }
    39
    40 for (auto& reader : readers) {
    41 reader.join();
    42 }
    43 for (auto& writer : writers) {
    44 writer.join();
    45 }
    46
    47 return 0;
    48 }

    代码解释
    上述代码示例模拟了读多写少的场景。多个 reader_thread 并发执行读操作,使用共享锁保护,互不阻塞。writer_thread 执行写操作,使用排他锁保护,写操作会阻塞读操作和其他写操作,但读操作之间可以并发执行,充分利用了 SharedMutex 的读写分离特性,提高了并发性能。

    4.2 避免死锁和活锁 (Avoiding Deadlocks and Livelocks)

    死锁 (Deadlock) 和 活锁 (Livelock) 是并发编程中常见的并发问题,它们会导致程序停滞不前或无限循环,严重影响程序的可用性和性能。本节将介绍如何避免在使用 folly::SharedMutex 时出现死锁和活锁。

    4.2.1 死锁的成因与预防 (Causes and Prevention of Deadlocks)

    死锁是指两个或多个线程因互相等待对方释放资源而无限期地阻塞的现象。死锁的发生通常需要满足以下四个必要条件:

    互斥条件 (Mutual Exclusion): 至少有一个资源必须处于非共享模式,即一次只有一个线程可以使用该资源。SharedMutex 的排他锁满足互斥条件。

    占有并等待条件 (Hold and Wait): 至少有一个线程必须持有一个资源,并且还在等待获取其他线程持有的资源。

    不可剥夺条件 (No Preemption): 线程已获得的资源在未使用完之前,不能被强制剥夺,只能由持有线程主动释放。

    循环等待条件 (Circular Wait): 存在一个线程等待资源的环形链,例如,线程 A 等待线程 B 持有的资源,线程 B 等待线程 C 持有的资源,线程 C 等待线程 A 持有的资源。

    要预防死锁,只需要破坏这四个必要条件中的一个或多个。

    破坏循环等待条件
    这是最常用的死锁预防方法。通过为所有锁定义一个全局的获取顺序,并要求所有线程都按照这个顺序获取锁,可以避免循环等待的发生。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 folly::SharedMutex mutex1, mutex2;
    2
    3 void thread_func_1() {
    4 mutex1.lock(); // 先获取 mutex1
    5 // ...
    6 mutex2.lock(); // 后获取 mutex2
    7 // ...
    8 mutex2.unlock();
    9 mutex1.unlock();
    10 }
    11
    12 void thread_func_2() {
    13 mutex1.lock(); // 同样先获取 mutex1
    14 // ...
    15 mutex2.lock(); // 后获取 mutex2
    16 // ...
    17 mutex2.unlock();
    18 mutex1.unlock();
    19 }

    代码解释
    在上述代码中,所有线程都按照 mutex1 -> mutex2 的顺序获取锁,破坏了循环等待条件,从而避免了死锁的发生。

    破坏占有并等待条件
    可以采用一次性申请所有需要的资源的方法,或者在等待新资源时先释放已占有的资源。但这在实际应用中可能比较复杂,且可能降低并发度。

    破坏不可剥夺条件
    允许操作系统剥夺线程已获得的资源。但这通常需要操作系统的支持,且实现复杂,开销较大。

    破坏互斥条件
    将独占资源改造为共享资源。但这通常不可行,因为互斥性是锁的核心目的。

    在实际应用中,破坏循环等待条件 是最常用且有效的死锁预防方法。为锁定义清晰的获取顺序,并严格遵守这个顺序,可以有效地避免死锁。

    4.2.2 活锁的成因与预防 (Causes and Prevention of Livelocks)

    活锁是指多个线程为了避免死锁而不断地尝试获取锁,但由于某些策略或算法的限制,导致所有线程都无法成功获取锁,从而无限循环地进行重试的现象。活锁与死锁不同,活锁中的线程并没有被阻塞,而是在不断地运行,但却无法取得进展。

    竞争性重试策略
    活锁通常发生在使用了竞争性重试策略的场景中。例如,多个线程同时尝试获取多个锁,如果获取失败则回退并重试。如果重试策略不当,例如,所有线程都同时回退并同时重试,就可能导致活锁。

    优先级反转
    在某些优先级调度系统中,低优先级线程持有锁,高优先级线程等待锁,但由于调度策略的原因,低优先级线程迟迟得不到调度,导致高优先级线程一直等待,也可能形成活锁。

    预防活锁的方法主要有以下几种:

    随机退避 (Randomized Backoff)
    当线程尝试获取锁失败时,不是立即重试,而是等待一个随机的时间间隔后再重试。这样可以错开多个线程的重试时间,降低竞争冲突的可能性,从而避免活锁。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <thread>
    3 #include <random>
    4 #include <iostream>
    5
    6 folly::SharedMutex mutex;
    7 std::random_device rd;
    8 std::mt19937 gen(rd());
    9 std::uniform_int_distribution<> distrib(10, 100); // 随机等待时间范围
    10
    11 void thread_func(int id) {
    12 for (int i = 0; i < 5; ++i) {
    13 while (true) {
    14 if (mutex.try_lock()) { // 尝试获取排他锁
    15 std::cout << "Thread " << id << " acquired lock." << std::endl;
    16 std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 模拟临界区操作
    17 mutex.unlock();
    18 std::cout << "Thread " << id << " released lock." << std::endl;
    19 break;
    20 } else {
    21 int wait_time = distrib(gen); // 生成随机等待时间
    22 std::cout << "Thread " << id << " try_lock failed, waiting for " << wait_time << "ms." << std::endl;
    23 std::this_thread::sleep_for(std::chrono::milliseconds(wait_time)); // 随机等待
    24 }
    25 }
    26 }
    27 }
    28
    29 int main() {
    30 std::thread t1(thread_func, 1);
    31 std::thread t2(thread_func, 2);
    32
    33 t1.join();
    34 t2.join();
    35
    36 return 0;
    37 }

    代码解释
    在上述代码中,当 try_lock 获取锁失败时,线程会等待一个随机的时间间隔后再重试。随机退避策略可以有效地避免多个线程同时重试导致的活锁问题。

    优先级调度
    在优先级调度系统中,合理设置线程优先级,避免优先级反转。可以使用优先级继承或优先级天花板等技术来解决优先级反转问题。

    排队机制
    引入公平的排队机制,例如,先来先服务 (FCFS) 队列,当多个线程竞争锁时,按照请求顺序排队,避免线程饥饿和活锁。folly::SharedMutex 内部的等待队列在一定程度上也起到了排队的作用。

    避免无限重试
    限制线程重试获取锁的次数或时间,当重试次数或时间超过阈值时,放弃重试,并采取其他处理策略(例如,报错、回退)。

    综合使用随机退避、优先级调度、排队机制等策略,可以有效地预防活锁的发生,提高并发程序的健壮性和可靠性。

    4.2.3 使用超时锁避免死锁和活锁 (Using Timeout Locks to Avoid Deadlocks and Livelocks)

    超时锁 (Timeout Lock) 是一种在尝试获取锁时设置超时时间的锁机制。如果在指定的时间内未能成功获取锁,则 try_lock_fortry_lock_until 等 API 会返回失败,线程可以根据返回值判断是否获取锁成功,并采取相应的处理措施。超时锁可以有效地避免因无限等待锁而导致的死锁和活锁。

    try_lock_fortry_lock_until
    folly::SharedMutex 提供了 try_lock_fortry_lock_until 等 API,用于尝试在指定时间内获取排他锁或共享锁。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <chrono>
    3 #include <thread>
    4 #include <iostream>
    5
    6 folly::SharedMutex mutex;
    7
    8 void thread_func() {
    9 auto start_time = std::chrono::steady_clock::now();
    10 auto timeout_time = start_time + std::chrono::milliseconds(500); // 设置超时时间为 500ms
    11
    12 if (mutex.try_lock_until(timeout_time)) { // 尝试在超时时间内获取排他锁
    13 std::cout << "Thread acquired lock within timeout." << std::endl;
    14 std::this_thread::sleep_for(std::chrono::milliseconds(300)); // 模拟临界区操作
    15 mutex.unlock();
    16 } else {
    17 std::cout << "Thread failed to acquire lock within timeout." << std::endl;
    18 // 超时处理逻辑
    19 }
    20 }
    21
    22 int main() {
    23 std::thread t1(thread_func);
    24 std::thread t2(thread_func);
    25
    26 t1.join();
    27 t2.join();
    28
    29 return 0;
    30 }

    代码解释
    在上述代码中,try_lock_until(timeout_time) 尝试在 500ms 内获取排他锁。如果在超时时间内成功获取锁,则执行临界区代码并释放锁;如果超时后仍未获取到锁,则返回 false,线程可以根据返回值进行超时处理,例如,记录日志、重试、放弃操作等。

    死锁检测与恢复
    超时锁可以与死锁检测机制结合使用。当检测到可能发生死锁时,可以使用超时锁尝试获取锁,如果超时后仍未获取到锁,则可以判断可能发生了死锁,并采取死锁恢复措施,例如,回滚事务、重启线程等。

    活锁检测与避免
    超时锁也可以用于检测活锁。如果线程在一定时间内反复尝试获取锁但总是失败,且没有被阻塞,则可能发生了活锁。可以使用超时锁限制线程的重试次数或时间,避免无限循环重试导致的活锁。

    使用超时锁可以有效地提高并发程序的健壮性,避免因无限等待锁而导致的死锁和活锁问题。在设计并发程序时,应根据实际需求合理设置超时时间,并提供相应的超时处理逻辑。

    4.3 性能调优与监控 (Performance Tuning and Monitoring)

    性能调优和监控是保证并发程序高效运行的关键环节。通过合理的性能调优,可以最大限度地发挥 folly::SharedMutex 的性能优势,避免性能瓶颈。通过性能监控,可以及时发现和解决潜在的性能问题。

    4.3.1 性能指标监控 (Performance Metrics Monitoring)

    监控关键的性能指标是进行性能调优的基础。对于使用 folly::SharedMutex 的并发程序,需要重点监控以下性能指标:

    锁竞争率 (Lock Contention Rate)
    锁竞争率是指线程在尝试获取锁时发生竞争的频率。高锁竞争率通常意味着线程需要频繁地等待锁的释放,导致线程阻塞和上下文切换,降低并发性能。可以使用性能分析工具或自定义监控代码来统计锁竞争率。

    平均锁持有时间 (Average Lock Holding Time)
    平均锁持有时间是指锁被线程持有的平均时长。锁持有时间越长,其他线程等待锁的时间也越长,并发度越低。可以使用性能分析工具或在代码中添加计时器来测量平均锁持有时间。

    吞吐量 (Throughput)
    吞吐量是指单位时间内系统完成的任务数量。吞吐量是衡量系统整体性能的重要指标。可以通过监控单位时间内完成的事务数、请求数等来评估吞吐量。

    延迟 (Latency)
    延迟是指从请求发出到收到响应的时间间隔。延迟是衡量系统响应速度的重要指标。对于并发程序,锁竞争可能导致延迟增加。可以通过监控请求的平均延迟、最大延迟等来评估延迟。

    CPU 利用率 (CPU Utilization)
    CPU 利用率是指 CPU 被程序使用的百分比。高 CPU 利用率并不一定意味着高性能,如果 CPU 主要用于线程上下文切换或空转等待锁,则高 CPU 利用率反而可能意味着性能瓶颈。需要结合其他指标综合分析。

    上下文切换次数 (Context Switch Count)
    上下文切换次数是指操作系统在不同线程之间切换执行的次数。频繁的上下文切换会消耗大量的 CPU 时间,降低系统性能。高锁竞争率可能导致上下文切换次数增加。

    可以使用以下工具进行性能监控:

    性能分析工具: 例如,Linux 下的 perfvalgrind,Windows 下的 Performance Monitor、VTune Amplifier 等性能分析工具,可以提供详细的性能数据,包括锁竞争、函数调用开销、CPU 使用率等。
    系统监控工具: 例如,tophtopvmstat 等系统监控工具,可以实时监控 CPU 利用率、内存使用率、进程状态等系统资源使用情况。
    自定义监控代码: 在代码中添加计时器、计数器等监控代码,可以收集特定的性能指标,例如,锁持有时间、事务处理时间等。可以使用 folly::Benchmark 库进行基准测试。

    4.3.2 锁性能调优策略 (Lock Performance Tuning Strategies)

    根据性能监控数据,可以采取相应的调优策略来提高 folly::SharedMutex 的性能。

    减少锁竞争
    细粒度锁: 如 4.1.1 节所述,使用细粒度锁可以减少锁的保护范围,降低锁竞争的可能性。
    读写分离: 如 4.1.5 节所述,对于读多写少的场景,使用读写分离策略,允许多个读线程并发执行,减少写操作对读操作的影响。
    减少临界区代码量: 如 4.1.2 节所述,优化临界区代码,减少临界区代码的执行时间,缩短锁持有时间。
    无锁数据结构: 在某些场景下,可以考虑使用无锁数据结构(例如,原子操作、无锁队列)代替锁,完全消除锁竞争。

    优化锁的实现
    folly::SharedMutex 已经做了很多性能优化,但在某些特定场景下,可能需要根据实际情况调整锁的实现参数。例如,调整自旋锁的自旋次数、调整等待队列的长度等。但这通常需要深入理解 folly::SharedMutex 的内部实现,并谨慎进行。

    硬件优化
    硬件环境对锁的性能也有影响。例如,使用多核 CPU 可以提高并发度,使用高速缓存可以减少内存访问延迟。在条件允许的情况下,可以考虑升级硬件设备来提高锁的性能。

    避免不必要的锁
    仔细检查代码,避免在不需要互斥访问共享资源的地方使用锁。例如,如果某个共享资源只在初始化阶段被写入一次,后续只读,则在初始化完成后可以不再需要锁保护。

    锁池技术
    对于频繁创建和销毁锁的场景,可以使用锁池技术,预先创建一批锁对象,并将其放入锁池中。当需要使用锁时,从锁池中获取一个锁对象,使用完后再将锁对象放回锁池,避免频繁的锁对象创建和销毁开销。

    4.3.3 监控工具与方法 (Monitoring Tools and Methods)

    选择合适的监控工具和方法对于性能调优至关重要。

    perf (Linux Performance Analyzer)
    perf 是 Linux 系统自带的强大的性能分析工具,可以用于分析 CPU 性能、内存性能、锁竞争等。perf 可以收集各种性能事件,例如,CPU 周期、指令数、缓存未命中、锁等待事件等,并生成性能报告。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 # 监控锁竞争事件
    2 perf record -e mutex:mutex_lock,mutex:mutex_unlock ./your_program
    3
    4 # 生成性能报告
    5 perf report

    valgrind (Memory and Threading Error Detector)
    valgrind 是一套强大的程序调试和性能分析工具集,其中包括 HelgrindDRD 等工具,可以用于检测线程竞争条件、死锁等并发错误,并提供性能分析数据。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 # 使用 Helgrind 检测线程竞争条件
    2 valgrind --tool=helgrind ./your_program
    3
    4 # 使用 DRD 检测数据竞争
    5 valgrind --tool=drd ./your_program

    folly::Benchmark (Benchmarking Library)
    folly::Benchmark 是 Facebook 开源的基准测试库,可以方便地进行代码性能基准测试。可以使用 folly::Benchmark 测量临界区代码的执行时间、锁的获取和释放开销等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/Benchmark.h>
    2 #include <folly/SharedMutex.h>
    3
    4 folly::SharedMutex mutex;
    5
    6 BENCHMARK(LockUnlockBenchmark, n) {
    7 for (int i = 0; i < n; ++i) {
    8 std::lock_guard<folly::SharedMutex> lock(mutex);
    9 // 临界区代码 (空操作)
    10 }
    11 }
    12
    13 int main() {
    14 folly::runBenchmarks();
    15 return 0;
    16 }

    自定义监控代码
    在代码中添加自定义监控代码,例如,使用 std::chrono 计时器测量锁持有时间、事务处理时间等,使用原子计数器统计锁竞争次数、请求处理次数等。可以将监控数据输出到日志文件、监控系统或可视化界面,进行实时监控和分析。

    选择合适的监控工具和方法,结合性能指标监控和性能调优策略,可以有效地提高 folly::SharedMutex 的性能,保证并发程序的高效运行。

    4.4 常见错误与问题排查 (Common Mistakes and Troubleshooting)

    在使用 folly::SharedMutex 的过程中,开发者可能会遇到各种各样的错误和问题。本节将总结一些常见的错误和问题,并提供相应的排查和解决指南。

    4.4.1 忘记释放锁 (Forgetting to Unlock)

    忘记释放锁是最常见的错误之一,会导致死锁或资源泄漏。

    错误示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2
    3 folly::SharedMutex mutex;
    4
    5 void bad_function() {
    6 mutex.lock(); // 获取锁
    7 // ... 临界区代码
    8 // 忘记释放锁
    9 } // 函数结束,但锁未释放,可能导致死锁

    问题分析
    在上述代码中,mutex.lock() 获取锁后,在函数 bad_function 结束前忘记调用 mutex.unlock() 释放锁。如果其他线程尝试获取该锁,将会被无限期地阻塞,导致死锁。

    解决方法
    使用 RAII 管理锁: 如 4.1.3 节所述,使用 std::lock_guardstd::shared_lock 等 RAII 锁管理类,可以确保锁在作用域结束时自动释放,避免忘记释放锁的错误。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void good_function() {
    2 std::lock_guard<folly::SharedMutex> lock(mutex); // RAII 自动管理锁
    3 // ... 临界区代码
    4 } // 作用域结束,lock 析构,自动释放锁

    代码审查: 仔细审查代码,确保在每个 lock()lock_shared() 之后都有对应的 unlock()unlock_shared() 调用。

    4.4.2 锁类型不匹配 (Lock Type Mismatch)

    在读写锁场景中,锁类型不匹配也是常见的错误。例如,在写操作时错误地使用了共享锁,或者在读操作时错误地使用了排他锁。

    错误示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2
    3 folly::SharedMutex mutex;
    4 int shared_value = 0;
    5
    6 void bad_writer() {
    7 mutex.lock_shared(); // 错误地使用了共享锁进行写操作
    8 shared_value++; // 存在数据竞争
    9 mutex.unlock_shared();
    10 }
    11
    12 void reader() {
    13 mutex.lock_shared();
    14 std::cout << "Value: " << shared_value << std::endl;
    15 mutex.unlock_shared();
    16 }

    问题分析
    bad_writer 函数中,错误地使用了 lock_shared() 获取共享锁进行写操作。由于共享锁允许多个线程同时持有,因此多个 bad_writer 线程可能同时修改 shared_value,导致数据竞争和数据不一致。

    解决方法
    正确选择锁类型: 在写操作时,必须使用排他锁 (lock, lock_guard) 保护,确保同一时刻只有一个线程可以修改共享资源。在读操作时,可以使用共享锁 (lock_shared, shared_lock) 允许多个线程并发读取。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void good_writer() {
    2 std::lock_guard<folly::SharedMutex> lock(mutex); // 正确使用排他锁进行写操作
    3 shared_value++;
    4 }

    代码审查: 仔细审查代码,确保在读操作和写操作时使用了正确的锁类型。

    4.4.3 死锁排查 (Deadlock Troubleshooting)

    死锁是并发编程中最棘手的问题之一。当程序发生死锁时,通常会表现为程序停滞不前,没有任何输出,CPU 利用率很低。

    死锁检测工具
    gdb 线程调试: 使用 gdb 等调试器可以查看线程的堆栈信息、锁状态等,帮助分析死锁原因。可以使用 thread apply all bt 命令查看所有线程的堆栈信息。
    pstack (Process Stack Trace)pstack 工具可以打印进程中所有线程的堆栈信息,用于快速定位死锁线程。
    操作系统提供的死锁检测工具: 某些操作系统或 JVM 提供了死锁检测工具,可以自动检测死锁并生成报告。

    死锁日志
    在程序中添加死锁检测日志,例如,在尝试获取锁之前和之后记录日志,记录线程 ID、锁对象地址等信息。当发生死锁时,可以分析日志信息,定位死锁发生的具体位置和锁竞争情况。

    代码审查
    仔细审查代码,特别是锁的获取和释放顺序,检查是否存在循环等待条件。如 4.2.1 节所述,为锁定义清晰的获取顺序,并严格遵守这个顺序,可以避免死锁。

    简化复现
    尝试简化死锁复现步骤,将问题代码隔离出来,编写最小化的可复现死锁的测试用例。这有助于更快速地定位和解决死锁问题。

    4.4.4 活锁排查 (Livelock Troubleshooting)

    活锁虽然不会导致程序阻塞,但会导致程序无限循环地进行无意义的操作,无法取得进展。活锁通常表现为程序 CPU 利用率很高,但吞吐量很低。

    性能监控
    监控程序的 CPU 利用率、吞吐量、延迟等性能指标。如果发现 CPU 利用率很高,但吞吐量很低,延迟很高,则可能发生了活锁。

    日志分析
    在代码中添加日志,记录线程的锁获取和释放情况、重试次数、等待时间等信息。分析日志信息,可以了解线程是否在不断地重试获取锁,但总是失败,从而判断是否发生了活锁。

    调整重试策略
    如 4.2.2 节所述,活锁通常与不合理的重试策略有关。可以尝试调整重试策略,例如,使用随机退避、限制重试次数等,避免活锁。

    代码审查
    仔细审查代码,检查是否存在竞争性重试策略,以及重试策略是否合理。

    4.4.5 性能瓶颈排查 (Performance Bottleneck Troubleshooting)

    性能瓶颈是指限制程序性能的关键因素。锁竞争是并发程序常见的性能瓶颈之一。

    性能分析工具
    使用性能分析工具(例如,perfvalgrind)分析程序的性能瓶颈。性能分析工具可以提供详细的性能数据,包括 CPU 使用率、内存使用率、锁竞争情况、函数调用开销等,帮助定位性能瓶颈。

    火焰图 (Flame Graph)
    火焰图是一种可视化性能分析结果的工具,可以将性能分析数据以火焰图的形式展示,直观地展示程序的 CPU 消耗情况、函数调用关系、热点代码等,帮助快速定位性能瓶颈。

    基准测试 (Benchmark)
    使用基准测试工具(例如,folly::Benchmark)对关键代码段进行基准测试,测量代码的执行时间、吞吐量等性能指标。通过基准测试,可以评估代码的性能,并进行性能优化。

    代码审查与优化
    根据性能分析结果,审查代码,找出性能瓶颈所在的代码段。针对性能瓶颈进行代码优化,例如,减少临界区代码量、使用更高效的算法和数据结构、减少内存分配和拷贝等。

    监控关键指标
    如 4.3.1 节所述,监控锁竞争率、平均锁持有时间、吞吐量、延迟等关键性能指标。根据监控数据,及时发现和解决性能瓶颈。

    通过以上排查和解决指南,可以帮助开发者有效地解决在使用 folly::SharedMutex 过程中遇到的各种常见错误和问题,提高程序的健壮性和性能。

    END_OF_CHAPTER

    5. chapter 5: SharedMutex API 全面解析 (Comprehensive API Analysis of SharedMutex)

    本章将深入探讨 folly/SharedMutex.h 提供的各种 API,旨在为读者提供一个全面而深入的 API 参考指南。无论你是初学者还是经验丰富的工程师,本章都将帮助你理解 SharedMutex 的每一个细节,从而在并发编程中更加灵活和高效地使用它。我们将从构造函数和析构函数开始,逐步深入到共享锁和排他锁的相关 API,最后介绍一些辅助 API,确保你对 SharedMutex 的 API 有一个彻底的掌握。

    5.1 构造函数与析构函数 (Constructors and Destructor)

    SharedMutex 提供了默认构造函数,以及拷贝构造函数和移动构造函数(虽然通常不推荐直接拷贝或移动互斥锁)。析构函数负责释放 SharedMutex 对象占用的资源。

    构造函数

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 SharedMutex();
    2 SharedMutex(const SharedMutex&) = delete; // Deleted copy constructor
    3 SharedMutex(SharedMutex&&) = delete; // Deleted move constructor

    SharedMutex(): 默认构造函数。创建一个未锁定的 SharedMutex 对象。这是最常用的构造方式。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2
    3 folly::SharedMutex mutex; // 创建一个 SharedMutex 对象

    拷贝构造函数和移动构造函数: SharedMutex 的拷贝构造函数和移动构造函数被显式删除 (= delete)。这意味着你不能直接拷贝或移动 SharedMutex 对象。这是为了防止多个 SharedMutex 对象管理同一个锁状态,从而导致逻辑错误和资源管理混乱。互斥锁通常代表的是一种独占性资源的访问控制机制,其状态不应该被简单地复制或转移。如果需要共享互斥锁,应该考虑使用指针引用来共享同一个 SharedMutex 对象。

    析构函数

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 ~SharedMutex();

    ~SharedMutex(): 析构函数。当 SharedMutex 对象生命周期结束时,析构函数会被自动调用。析构函数负责释放 SharedMutex 对象占用的所有资源。重要提示:在 SharedMutex 对象被销毁之前,必须确保所有持有该锁的线程都已经释放了锁。如果在持有锁的情况下销毁 SharedMutex 对象,会导致未定义行为。通常情况下,应该仔细设计程序的生命周期和锁的管理,确保在 SharedMutex 对象销毁前,锁已经被正确释放。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2
    3 void testMutex() {
    4 folly::SharedMutex mutex;
    5 // ... 使用 mutex ...
    6 } // mutex 在函数结束时被销毁,析构函数 ~SharedMutex() 被调用

    5.2 共享锁相关 API (Shared Lock Related APIs)

    共享锁(Shared Lock),也称为读锁(Read Lock),允许多个线程同时持有锁,只要它们都以共享模式访问受保护的资源。这非常适合于读多写少的场景,可以显著提高并发性能。SharedMutex 提供了以下 API 来管理共享锁:

    5.2.1 lock_shared()

    功能: 尝试获取共享锁。如果当前没有线程持有排他锁(Exclusive Lock,写锁),则调用线程会成功获取共享锁并继续执行。如果当前有线程持有排他锁,则调用线程会被阻塞,直到排他锁被释放,并且没有其他线程在等待获取排他锁。

    声明:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void lock_shared();

    行为:
    阻塞: lock_shared() 是一个阻塞操作。如果无法立即获取共享锁,调用线程会被挂起,直到可以获取锁为止。
    共享访问: 允许多个线程同时持有共享锁,实现并发读取
    优先级: 在锁竞争激烈的情况下,SharedMutex 的具体实现可能会有公平性或非公平性的策略,但这通常不应被依赖。一般而言,不应假设 lock_shared() 的获取顺序。

    异常: lock_shared() 不会抛出异常。

    使用场景: 当线程需要读取共享资源时,应该使用 lock_shared() 获取共享锁。这允许多个读者并发访问,提高读取性能。

    代码示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <iostream>
    3 #include <thread>
    4 #include <chrono>
    5
    6 folly::SharedMutex sharedMutex;
    7 int sharedData = 0;
    8
    9 void reader(int id) {
    10 for (int i = 0; i < 3; ++i) {
    11 sharedMutex.lock_shared(); // 获取共享锁
    12 std::cout << "Reader " << id << " read data: " << sharedData << std::endl;
    13 std::this_thread::sleep_for(std::chrono::milliseconds(100));
    14 sharedMutex.unlock_shared(); // 释放共享锁
    15 }
    16 }
    17
    18 int main() {
    19 std::thread reader1(reader, 1);
    20 std::thread reader2(reader, 2);
    21
    22 reader1.join();
    23 reader2.join();
    24
    25 return 0;
    26 }
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 在这个例子中,多个 `reader` 线程并发地读取 `sharedData`,它们都使用 `lock_shared()` 获取共享锁,允许多个读者同时访问。

    5.2.2 unlock_shared()

    功能: 释放之前通过 lock_shared()try_lock_shared() 获取的共享锁。

    声明:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void unlock_shared();

    行为:
    释放锁: unlock_shared() 释放调用线程持有的一个共享锁。如果线程多次调用 lock_shared(),则需要相应次数的 unlock_shared() 调用来完全释放锁。
    错误使用: 如果调用线程没有持有共享锁,或者释放的次数超过了获取的次数,行为是未定义的。必须保证 lock_shared()unlock_shared() 成对出现,且在正确的线程上下文中调用。

    异常: unlock_shared() 不会抛出异常。

    使用场景: 在完成对共享资源的读取操作后,必须及时调用 unlock_shared() 释放共享锁,以便其他线程可以获取锁并访问资源。

    代码示例: (参考 lock_shared() 的代码示例,unlock_shared() 的使用已经包含在其中)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // ... (接上例) ...
    2 void reader(int id) {
    3 for (int i = 0; i < 3; ++i) {
    4 sharedMutex.lock_shared();
    5 std::cout << "Reader " << id << " read data: " << sharedData << std::endl;
    6 std::this_thread::sleep_for(std::chrono::milliseconds(100));
    7 sharedMutex.unlock_shared(); // 释放共享锁
    8 }
    9 }
    10 // ...
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 `reader` 函数中,`unlock_shared()` 确保在读取操作完成后立即释放共享锁,允许其他读者或写者线程有机会获取锁。

    5.2.3 try_lock_shared()

    功能: 尝试非阻塞地获取共享锁。如果当前可以立即获取共享锁(即没有线程持有排他锁),则成功获取并返回 true。否则,立即返回 false,不会阻塞调用线程。

    声明:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 bool try_lock_shared();

    返回值:
    true: 成功获取共享锁。
    false: 未能获取共享锁。

    行为:
    非阻塞: try_lock_shared() 是一个非阻塞操作。它会立即返回,无论是否成功获取锁。
    条件获取: 只有在没有排他锁持有的情况下,才能成功获取共享锁。
    轮询: 如果 try_lock_shared() 返回 false,调用线程可以选择稍后重试,或者执行其他操作。

    异常: try_lock_shared() 不会抛出异常。

    使用场景:
    非阻塞读取: 当需要在不阻塞的情况下尝试读取共享资源时,可以使用 try_lock_shared()。例如,在某些实时性要求较高的系统中,不希望因为等待锁而导致线程长时间阻塞。
    避免死锁: 在某些复杂的锁交互场景中,使用 try_lock_shared() 可以帮助避免潜在的死锁问题。
    轮询重试: 可以结合循环和延迟,实现轮询式的锁获取尝试。

    代码示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <iostream>
    3 #include <thread>
    4 #include <chrono>
    5
    6 folly::SharedMutex sharedMutex;
    7 int sharedData = 0;
    8
    9 void nonBlockingReader(int id) {
    10 for (int i = 0; i < 3; ++i) {
    11 if (sharedMutex.try_lock_shared()) { // 尝试获取共享锁,非阻塞
    12 std::cout << "Non-blocking Reader " << id << " read data: " << sharedData << std::endl;
    13 std::this_thread::sleep_for(std::chrono::milliseconds(50));
    14 sharedMutex.unlock_shared(); // 释放共享锁
    15 } else {
    16 std::cout << "Non-blocking Reader " << id << " failed to acquire lock, retrying..." << std::endl;
    17 std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 稍后重试
    18 }
    19 }
    20 }
    21
    22 int main() {
    23 std::thread nbReader1(nonBlockingReader, 1);
    24 std::thread nbReader2(nonBlockingReader, 2);
    25
    26 nbReader1.join();
    27 nbReader2.join();
    28
    29 return 0;
    30 }
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 在这个例子中,`nonBlockingReader` 线程使用 `try_lock_shared()` 尝试获取共享锁。如果获取成功,则读取数据并释放锁;如果获取失败,则输出提示信息并稍后重试,而不会阻塞线程的执行。

    5.3 排他锁相关 API (Exclusive Lock Related APIs)

    排他锁(Exclusive Lock),也称为写锁(Write Lock),确保在任何时刻只有一个线程可以持有锁。当线程需要修改共享资源时,必须获取排他锁,以保证数据的一致性和完整性。SharedMutex 提供了以下 API 来管理排他锁:

    5.3.1 lock()

    功能: 尝试获取排他锁。如果当前没有线程持有任何锁(共享锁或排他锁),则调用线程会成功获取排他锁并继续执行。如果当前有线程持有任何类型的锁,则调用线程会被阻塞,直到所有锁都被释放,并且没有其他线程在等待获取排他锁。

    声明:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void lock();

    行为:
    阻塞: lock() 是一个阻塞操作。如果无法立即获取排他锁,调用线程会被挂起,直到可以获取锁为止。
    独占访问: 排他锁保证独占访问。在持有排他锁期间,没有其他线程可以获取共享锁或排他锁。
    优先级: 与 lock_shared() 类似,不应假设 lock() 的获取顺序。

    异常: lock() 不会抛出异常。

    使用场景: 当线程需要修改共享资源时,必须使用 lock() 获取排他锁。这保证了在修改操作期间,没有其他线程可以访问(读取或写入)该资源,从而避免数据竞争和不一致性。

    代码示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <iostream>
    3 #include <thread>
    4 #include <chrono>
    5
    6 folly::SharedMutex sharedMutex;
    7 int sharedData = 0;
    8
    9 void writer(int id) {
    10 for (int i = 0; i < 3; ++i) {
    11 sharedMutex.lock(); // 获取排他锁
    12 std::cout << "Writer " << id << " writing data..." << std::endl;
    13 sharedData++; // 修改共享数据
    14 std::this_thread::sleep_for(std::chrono::milliseconds(200));
    15 std::cout << "Writer " << id << " finished writing, data: " << sharedData << std::endl;
    16 sharedMutex.unlock(); // 释放排他锁
    17 }
    18 }
    19
    20 int main() {
    21 std::thread writer1(writer, 1);
    22 std::thread writer2(writer, 2);
    23
    24 writer1.join();
    25 writer2.join();
    26
    27 return 0;
    28 }
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 在这个例子中,多个 `writer` 线程尝试修改 `sharedData`。它们都使用 `lock()` 获取排他锁,保证在任何时刻只有一个写者可以修改数据,避免了数据竞争。

    5.3.2 unlock()

    功能: 释放之前通过 lock()try_lock() 获取的排他锁。

    声明:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void unlock();

    行为:
    释放锁: unlock() 释放调用线程持有的一个排他锁。如果线程多次调用 lock()(虽然不常见),则需要相应次数的 unlock() 调用来完全释放锁。
    错误使用: 如果调用线程没有持有排他锁,或者释放的次数超过了获取的次数,行为是未定义的。必须保证 lock()unlock() 成对出现,且在正确的线程上下文中调用。

    异常: unlock() 不会抛出异常。

    使用场景: 在完成对共享资源的修改操作后,必须及时调用 unlock() 释放排他锁,以便其他线程可以获取锁并访问资源(无论是读取还是写入)。

    代码示例: (参考 lock() 的代码示例,unlock() 的使用已经包含在其中)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // ... (接上例) ...
    2 void writer(int id) {
    3 for (int i = 0; i < 3; ++i) {
    4 sharedMutex.lock();
    5 std::cout << "Writer " << id << " writing data..." << std::endl;
    6 sharedData++;
    7 std::this_thread::sleep_for(std::chrono::milliseconds(200));
    8 std::cout << "Writer " << id << " finished writing, data: " << sharedData << std::endl;
    9 sharedMutex.unlock(); // 释放排他锁
    10 }
    11 }
    12 // ...
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 `writer` 函数中,`unlock()` 确保在写入操作完成后立即释放排他锁,允许其他读者或写者线程有机会获取锁。

    5.3.3 try_lock()

    功能: 尝试非阻塞地获取排他锁。如果当前可以立即获取排他锁(即没有线程持有任何锁),则成功获取并返回 true。否则,立即返回 false,不会阻塞调用线程。

    声明:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 bool try_lock();

    返回值:
    true: 成功获取排他锁。
    false: 未能获取排他锁。

    行为:
    非阻塞: try_lock() 是一个非阻塞操作。它会立即返回,无论是否成功获取锁。
    条件获取: 只有在没有其他线程持有任何锁的情况下,才能成功获取排他锁。
    轮询: 如果 try_lock() 返回 false,调用线程可以选择稍后重试,或者执行其他操作。

    异常: try_lock() 不会抛出异常。

    使用场景:
    非阻塞写入: 当需要在不阻塞的情况下尝试修改共享资源时,可以使用 try_lock()
    避免死锁: 与 try_lock_shared() 类似,try_lock() 也可以用于避免死锁。
    条件性更新: 在某些场景下,可能需要在满足特定条件时才进行写入操作,而 try_lock() 可以用于实现这种条件性更新。

    代码示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <iostream>
    3 #include <thread>
    4 #include <chrono>
    5
    6 folly::SharedMutex sharedMutex;
    7 int sharedData = 0;
    8
    9 void nonBlockingWriter(int id) {
    10 for (int i = 0; i < 3; ++i) {
    11 if (sharedMutex.try_lock()) { // 尝试获取排他锁,非阻塞
    12 std::cout << "Non-blocking Writer " << id << " writing data..." << std::endl;
    13 sharedData++;
    14 std::this_thread::sleep_for(std::chrono::milliseconds(150));
    15 std::cout << "Non-blocking Writer " << id << " finished writing, data: " << sharedData << std::endl;
    16 sharedMutex.unlock(); // 释放排他锁
    17 } else {
    18 std::cout << "Non-blocking Writer " << id << " failed to acquire lock, retrying..." << std::endl;
    19 std::this_thread::sleep_for(std::chrono::milliseconds(30)); // 稍后重试
    20 }
    21 }
    22 }
    23
    24 int main() {
    25 std::thread nbWriter1(nonBlockingWriter, 1);
    26 std::thread nbWriter2(nonBlockingWriter, 2);
    27
    28 nbWriter1.join();
    29 nbWriter2.join();
    30
    31 return 0;
    32 }
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 在这个例子中,`nonBlockingWriter` 线程使用 `try_lock()` 尝试获取排他锁。如果获取成功,则修改数据并释放锁;如果获取失败,则输出提示信息并稍后重试,而不会阻塞线程的执行。

    5.4 其他辅助 API (Other Auxiliary APIs)

    除了基本的锁获取和释放 API,SharedMutex 还提供了一些辅助 API,用于查询锁的状态或进行更细粒度的控制。虽然 folly/SharedMutex.h 的 API 相对精简,但通常与 RAII 锁守卫(Lock Guards)一起使用,以提供更安全和方便的锁管理。

    folly/SharedMutex.h 中,核心的辅助 API 主要体现在与 Lock Guards 的配合使用上,例如 folly::SharedLockGuardfolly::ExclusiveLockGuard。这些 Lock Guards 并非 SharedMutex 的成员函数,但它们是使用 SharedMutex 时非常重要的辅助工具。

    Lock Guards (锁守卫): folly::SharedLockGuardfolly::ExclusiveLockGuard 是 RAII (Resource Acquisition Is Initialization) 风格的锁管理类。它们在构造时尝试获取锁,并在析构时自动释放锁,从而避免了手动管理锁的繁琐和容易出错的问题。

    ▮▮▮▮⚝ folly::SharedLockGuard<SharedMutex>: 用于管理共享锁。在构造时调用 mutex.lock_shared() 获取共享锁,在析构时调用 mutex.unlock_shared() 释放共享锁。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <folly/SharedMutex-inl.h> // 需要包含 -inl.h 文件
    3 #include <iostream>
    4
    5 folly::SharedMutex mutex;
    6 int sharedValue = 0;
    7
    8 void readData() {
    9 folly::SharedLockGuard<folly::SharedMutex> lock(mutex); // RAII 风格获取共享锁
    10 std::cout << "Reading data: " << sharedValue << std::endl;
    11 // ... 在锁保护下访问共享资源 ...
    12 } // lock 对象析构时,自动释放共享锁

    ▮▮▮▮⚝ folly::ExclusiveLockGuard<SharedMutex>: 用于管理排他锁。在构造时调用 mutex.lock() 获取排他锁,在析构时调用 mutex.unlock() 释放排他锁。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <folly/SharedMutex.h>
    2 #include <folly/SharedMutex-inl.h> // 需要包含 -inl.h 文件
    3 #include <iostream>
    4
    5 folly::SharedMutex mutex;
    6 int sharedValue = 0;
    7
    8 void writeData() {
    9 folly::ExclusiveLockGuard<folly::SharedMutex> lock(mutex); // RAII 风格获取排他锁
    10 std::cout << "Writing data..." << std::endl;
    11 sharedValue++;
    12 // ... 在锁保护下修改共享资源 ...
    13 } // lock 对象析构时,自动释放排他锁
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 **注意**: 为了使用 `folly::SharedLockGuard` 和 `folly::ExclusiveLockGuard`,通常需要包含 `-inl.h` 文件,例如 `folly/SharedMutex-inl.h`。这是 folly 库中 inline 模板实现的常见做法。

    虽然 folly/SharedMutex.h 本身提供的 API 相对简洁,但结合 Lock Guards 和其他 folly 库的工具,可以构建强大且高效的并发程序。在实际应用中,推荐使用 Lock Guards 来管理 SharedMutex 的生命周期,以提高代码的安全性、可读性和可维护性。

    END_OF_CHAPTER

    6. chapter 6: SharedMutex 高级主题与展望 (Advanced Topics and Future Trends of SharedMutex)

    6.1 SharedMutex 在大型系统中的应用 (Application of SharedMutex in Large-Scale Systems)

    在大型系统中,高并发和高性能是至关重要的设计目标。SharedMutex.h 作为一种高效的读写锁(Reader-Writer Lock),在应对读多写少的并发场景时,能够发挥关键作用。本节将深入探讨 SharedMutex.h 在大型系统中的应用,并分析其如何助力构建可扩展、高性能的并发系统。

    大型系统通常面临以下并发挑战:

    高并发访问:大量用户或服务同时访问共享资源,例如数据库、缓存、配置中心等。
    读多写少场景: 许多系统,特别是互联网应用,读操作远多于写操作。例如,网页浏览、商品查询、信息流读取等。
    低延迟要求: 用户对响应时间非常敏感,系统需要快速响应用户请求。
    数据一致性: 在并发环境下,必须保证数据的一致性和完整性。

    SharedMutex.h 正是为解决这类挑战而设计的。它允许多个读者同时访问共享资源,而写者在写入时则独占访问,从而在保证数据一致性的前提下,最大程度地提升并发性能。

    应用场景示例:

    高并发缓存系统: 缓存是大型系统中常用的性能优化手段。使用 SharedMutex.h 可以保护缓存数据的并发访问。
    ▮▮▮▮⚝ 读取缓存: 多个线程可以同时获取共享锁(Shared Lock)读取缓存数据,提高读取并发度。
    ▮▮▮▮⚝ 更新缓存: 当需要更新缓存时,线程可以获取排他锁(Exclusive Lock),独占式更新缓存,保证数据一致性。
    分布式配置中心: 配置中心存储着系统的各种配置信息,这些信息通常被频繁读取,但更新频率较低。
    ▮▮▮▮⚝ 读取配置: 各个服务实例可以并发地读取配置信息,提高配置获取效率。
    ▮▮▮▮⚝ 更新配置: 管理后台或配置发布系统可以获取排他锁更新配置,保证配置更新的原子性。
    元数据管理: 在分布式存储系统或文件系统中,元数据(Metadata)的访问模式通常是读多写少。例如,文件目录信息、对象存储的元数据等。
    ▮▮▮▮⚝ 读取元数据: 多个客户端可以并发读取元数据信息,加速元数据查询。
    ▮▮▮▮⚝ 更新元数据: 当文件或对象发生变更时,需要排他地更新元数据,维护数据一致性。
    数据库连接池: 连接池管理着数据库连接资源,多个线程需要共享连接池获取数据库连接。
    ▮▮▮▮⚝ 获取连接: 多个线程可以并发地从连接池获取连接,提高连接获取效率。
    ▮▮▮▮⚝ 归还连接: 归还连接的操作也需要一定的同步机制,但通常读操作(获取连接)远多于写操作(归还连接和创建连接)。

    性能考量与最佳实践:

    锁粒度控制: 合理控制锁的粒度至关重要。过大的锁粒度会降低并发度,过小的锁粒度会增加锁管理的开销。在大型系统中,需要根据具体的业务场景和数据访问模式,选择合适的锁粒度。例如,可以考虑对不同的数据分区或数据对象使用不同的 SharedMutex.h 实例。
    减少锁竞争: 尽量减少写操作的频率和持续时间,以降低排他锁的竞争。可以通过缓存、批量更新、异步写入等技术手段来优化写操作。
    读写偏斜优化: 针对读多写少的场景,SharedMutex.h 已经做了很好的优化。但仍然需要注意避免写操作过于集中,导致读操作被阻塞。可以考虑使用更细粒度的锁,或者采用分段锁(Segmented Lock)等技术,进一步提升并发性能。
    监控与调优: 在大型系统中,性能监控至关重要。需要监控 SharedMutex.h 的锁竞争情况、等待时间等指标,及时发现性能瓶颈并进行调优。可以使用 Folly 提供的性能分析工具,或者集成到现有的监控系统中。

    总结:

    SharedMutex.h 在大型系统中,尤其是在读多写少的并发场景下,是一种非常有效的并发控制工具。通过合理地应用 SharedMutex.h,可以显著提升系统的并发性能和响应速度,构建可扩展、高性能的大型系统。然而,也需要注意锁粒度控制、减少锁竞争、读写偏斜优化以及性能监控等方面,才能充分发挥 SharedMutex.h 的优势。

    6.2 SharedMutex 的扩展与定制 (Extension and Customization of SharedMutex)

    虽然 folly::SharedMutex 已经提供了强大且高效的读写锁功能,但在某些特定的应用场景下,我们可能需要对其进行扩展或定制,以满足更特殊的需求。本节将探讨 SharedMutex 的扩展与定制方法,并分析其潜在的应用场景和注意事项。

    扩展与定制的方向:

    公平性 (Fairness): 默认情况下,SharedMutex 不保证公平性,即等待时间长的线程可能不会优先获得锁。在某些公平性敏感的应用中,例如服务质量 (QoS) 保证的系统,可能需要实现公平锁。
    ▮▮▮▮⚝ 实现思路: 可以通过在 SharedMutex 的内部维护等待队列,并按照先进先出 (FIFO) 的原则来唤醒等待线程。
    ▮▮▮▮⚝ 性能影响: 公平性通常会带来一定的性能开销,因为需要维护额外的队列和调度逻辑。需要在公平性和性能之间进行权衡。

    优先级 (Priority): 在某些系统中,不同线程的优先级不同,高优先级的线程应该优先获得锁。可以扩展 SharedMutex 以支持优先级调度。
    ▮▮▮▮⚝ 实现思路: 可以根据线程的优先级维护多个等待队列,或者在等待队列中使用优先级队列。
    ▮▮▮▮⚝ 应用场景: 实时系统、任务调度系统等。

    统计信息 (Statistics): 为了更好地监控和调优系统性能,可以扩展 SharedMutex 以收集锁的统计信息,例如:
    ▮▮▮▮⚝ 锁的获取次数
    ▮▮▮▮⚝ 锁的释放次数
    ▮▮▮▮⚝ 平均等待时间
    ▮▮▮▮⚝ 最大等待时间
    ▮▮▮▮⚝ 竞争次数
    ▮▮▮▮⚝ 持有锁的时间
    ▮▮▮▮⚝ 实现思路: 在 SharedMutex 的内部添加计数器和计时器,在锁的获取和释放操作中更新统计信息。
    ▮▮▮▮⚝ 应用场景: 性能监控系统、性能分析工具等。

    可中断锁 (Interruptible Lock): 在某些场景下,例如取消操作或超时处理,可能需要中断正在等待锁的线程。可以扩展 SharedMutex 以支持锁的中断。
    ▮▮▮▮⚝ 实现思路: 可以结合条件变量和信号机制,实现锁的中断功能。当需要中断等待线程时,可以发送信号通知条件变量,唤醒等待线程并抛出中断异常。
    ▮▮▮▮⚝ 应用场景: 任务取消、超时控制等。

    自旋锁优化 (Spin Lock Optimization): 对于短时间持有的锁,自旋锁可以减少线程上下文切换的开销。可以考虑在 SharedMutex 中引入自旋锁优化。
    ▮▮▮▮⚝ 实现思路: 在线程尝试获取锁时,先进行一段时间的自旋等待,如果自旋等待后仍然没有获得锁,再进入阻塞等待状态。
    ▮▮▮▮⚝ 优化策略: 自旋等待的时间需要根据具体的硬件平台和应用场景进行调整。过长的自旋等待会浪费 CPU 资源,过短的自旋等待则效果不明显。

    定制化的注意事项:

    复杂性增加: 扩展和定制 SharedMutex 会增加其内部实现的复杂性,可能引入新的 bug 或性能问题。需要充分测试和验证定制化的代码。
    维护成本: 定制化的 SharedMutex 可能与 Folly 库的后续版本不兼容,需要额外的维护成本。
    性能影响: 任何定制化都可能对性能产生影响,需要仔细评估性能开销,并进行性能测试。
    通用性降低: 定制化的 SharedMutex 可能只适用于特定的应用场景,通用性会降低。

    扩展方式:

    继承 (Inheritance): 可以通过继承 folly::SharedMutex 类,并重写或添加新的方法来实现扩展功能。
    组合 (Composition): 可以将 folly::SharedMutex 作为成员变量,并封装新的类来实现扩展功能。
    修改源码 (Modifying Source Code): 可以直接修改 folly::SharedMutex 的源码,但这通常不推荐,因为会增加维护成本,并且可能与 Folly 库的后续版本冲突。

    总结:

    SharedMutex 的扩展与定制可以满足特定应用场景的特殊需求,例如公平性、优先级、统计信息、可中断锁和自旋锁优化等。在进行扩展和定制时,需要权衡复杂性、维护成本、性能影响和通用性等因素,并选择合适的扩展方式。 建议优先考虑继承或组合的方式进行扩展,避免直接修改源码。

    6.3 并发编程的未来趋势 (Future Trends in Concurrent Programming)

    并发编程是软件开发中一个持续演进的领域。随着硬件架构的不断发展和应用场景的日益复杂,并发编程也在不断地涌现出新的趋势和技术。理解并发编程的未来趋势,有助于我们更好地应对未来的并发挑战,并选择合适的并发编程模型和工具。

    硬件发展趋势对并发编程的影响:

    多核/众核处理器 (Multi-core/Many-core Processors): 处理器核心数量的持续增加是硬件发展的主要趋势。未来的处理器将拥有更多的核心,甚至达到数百、数千个核心。这为并发编程提供了强大的硬件基础,但也对并发编程模型和技术提出了更高的要求。如何有效地利用大量的处理器核心,充分发挥硬件的并行能力,是未来并发编程需要解决的关键问题。
    异构计算 (Heterogeneous Computing): 除了 CPU,GPU、FPGA、ASIC 等异构计算设备在高性能计算、人工智能等领域发挥着越来越重要的作用。未来的计算平台将更加异构化,CPU、GPU 和其他加速器将协同工作。并发编程需要考虑如何有效地利用异构计算资源,实现任务在不同类型处理器之间的合理分配和调度。
    非均匀内存访问 (NUMA): NUMA 架构在多处理器系统中越来越常见。NUMA 架构下,不同处理器核心访问本地内存的速度远快于访问远程内存。并发编程需要考虑 NUMA 架构的特性,优化数据局部性,减少跨 NUMA 节点的内存访问,提高性能。
    新型内存技术 (Emerging Memory Technologies): 持久内存 (Persistent Memory)、高速缓存 (High-Bandwidth Memory) 等新型内存技术的出现,为并发编程带来了新的机遇和挑战。持久内存可以提供字节寻址、非易失性的内存,简化持久化编程模型。高速缓存可以提供更高的内存带宽和更低的延迟,提升并发程序的性能。

    软件发展趋势对并发编程的影响:

    异步编程 (Asynchronous Programming): 异步编程模型在现代并发编程中占据越来越重要的地位。异步编程可以避免线程阻塞,提高资源利用率和响应速度。例如,C++ 的 std::futurestd::async、协程 (Coroutines) 等特性,以及 JavaScript 的 Promise、async/await 等语法,都体现了异步编程的趋势。
    反应式编程 (Reactive Programming): 反应式编程是一种面向数据流和变化传播的编程范式。反应式编程可以简化异步事件处理、UI 交互、数据流处理等复杂并发场景的开发。例如,RxJava、Reactor、RxSwift 等反应式编程库在各个领域得到广泛应用。
    函数式编程 (Functional Programming): 函数式编程强调纯函数、不可变数据、无副作用等特性,可以提高并发程序的可靠性和可维护性。函数式编程语言,例如 Haskell、Scala、Erlang 等,在并发编程领域具有独特的优势。
    Actor 模型 (Actor Model): Actor 模型是一种基于消息传递的并发编程模型。Actor 模型将并发实体抽象为 Actor,Actor 之间通过消息进行通信,避免了共享状态和锁竞争,简化了并发编程的复杂性。例如,Erlang、Akka 等 Actor 模型框架在构建高并发、分布式系统方面表现出色。
    Lock-Free 和 Wait-Free 算法 (Lock-Free and Wait-Free Algorithms): 传统的锁机制在某些高并发场景下可能成为性能瓶颈。Lock-Free 和 Wait-Free 算法旨在避免使用锁,通过原子操作等技术实现并发数据结构的无锁访问。Lock-Free 和 Wait-Free 算法可以提高并发程序的性能和可伸缩性,但也更加复杂和难以实现。
    形式化验证 (Formal Verification): 并发程序的正确性验证是一个挑战。形式化验证技术可以对并发程序进行数学建模和验证,提高并发程序的可靠性。例如,TLA+、Coq 等形式化验证工具在并发系统设计和验证中得到应用。

    SharedMutex 在未来并发编程中的角色:

    SharedMutex 作为一种高效的读写锁,在未来并发编程中仍然具有重要的作用。在读多写少的并发场景下,SharedMutex 仍然是一种简单、有效、可靠的并发控制工具。

    与异步编程结合SharedMutex 可以与异步编程模型结合使用,例如,在异步任务中保护共享资源。
    在反应式系统中应用: 在反应式系统中,可以使用 SharedMutex 保护反应式数据流的并发访问。
    在 Actor 模型中作为底层同步机制: 在 Actor 模型的实现中,可以使用 SharedMutex 作为 Actor 内部状态的同步机制。
    在 Lock-Free 算法中作为辅助工具: 即使在 Lock-Free 算法中,也可能需要在某些场景下使用锁来保护初始化或其他非核心操作。SharedMutex 可以作为 Lock-Free 算法的辅助同步工具。

    总结与展望:

    并发编程的未来发展趋势是多元化的,硬件发展、软件模型、编程范式都在不断演进。异步编程、反应式编程、函数式编程、Actor 模型、Lock-Free 算法等技术将会在未来的并发编程中发挥越来越重要的作用。SharedMutex 作为一种经典的并发控制工具,在未来仍然具有重要的应用价值。我们需要根据具体的应用场景和需求,选择合适的并发编程模型和工具,构建高效、可靠、可扩展的并发系统。同时,也需要持续关注并发编程领域的新技术和新趋势,不断学习和进步,应对未来的并发挑战。

    END_OF_CHAPTER