028 《深入探索 Folly 库:原理、实践与高级应用》
🌟🌟🌟本文由Gemini 2.0 Flash Thinking Experimental 01-21生成,用来辅助学习。🌟🌟🌟
书籍大纲
▮▮ 1. Folly 库概览:起步与核心概念 (Overview of Folly Library: Getting Started and Core Concepts)
▮▮▮▮ 1.1 1.1 什么是 Folly 库? (What is Folly Library?)
▮▮▮▮▮▮ 1.1.1 1.1.1 Folly 的起源与发展 (Origin and Development of Folly)
▮▮▮▮▮▮ 1.1.2 1.1.2 Folly 的设计哲学与目标 (Design Philosophy and Goals of Folly)
▮▮▮▮▮▮ 1.1.3 1.1.3 Folly 的核心特性与模块 (Core Features and Modules of Folly)
▮▮▮▮ 1.2 1.2 快速上手:环境搭建与编译 (Quick Start: Environment Setup and Compilation)
▮▮▮▮▮▮ 1.2.1 1.2.1 依赖项安装与管理 (Dependency Installation and Management)
▮▮▮▮▮▮ 1.2.2 1.2.2 使用 CMake 构建 Folly 项目 (Building Folly Projects with CMake)
▮▮▮▮▮▮ 1.2.3 1.2.3 运行第一个 Folly 程序 (Running Your First Folly Program)
▮▮▮▮ 1.3 1.3 Folly 的命名空间与代码组织 (Folly Namespaces and Code Organization)
▮▮▮▮▮▮ 1.3.1 1.3.1 顶层命名空间 folly (Top-level Namespace folly)
▮▮▮▮▮▮ 1.3.2 1.3.2 模块化设计与头文件组织 (Modular Design and Header File Organization)
▮▮▮▮▮▮ 1.3.3 1.3.3 代码风格与约定 (Code Style and Conventions)
▮▮ 2. 字符串处理:强大的 folly::fbstring 与格式化 (String Processing: Powerful folly::fbstring and Formatting)
▮▮▮▮ 2.1 2.1 folly::fbstring:高效的字符串实现 (folly::fbstring: Efficient String Implementation)
▮▮▮▮▮▮ 2.1.1 2.1.1 fbstring 的内存管理与优化 (Memory Management and Optimization of fbstring)
▮▮▮▮▮▮ 2.1.2 2.1.2 fbstring 的常用操作与接口 (Common Operations and Interfaces of fbstring)
▮▮▮▮▮▮ 2.1.3 2.1.3 fbstring 与 std::string 的互操作性 (Interoperability between fbstring and std::string)
▮▮▮▮ 2.2 2.2 folly::format:灵活且类型安全的格式化 (folly::format: Flexible and Type-Safe Formatting)
▮▮▮▮▮▮ 2.2.1 2.2.1 format 语法与占位符 (format Syntax and Placeholders)
▮▮▮▮▮▮ 2.2.2 2.2.2 类型安全与编译时检查 (Type Safety and Compile-Time Checks)
▮▮▮▮▮▮ 2.2.3 2.2.3 自定义格式化器与扩展 (Custom Formatters and Extensions)
▮▮▮▮ 2.3 2.3 StringPiece:高效的字符串视图 (StringPiece: Efficient String View)
▮▮▮▮▮▮ 2.3.1 2.3.1 StringPiece 的原理与应用场景 (Principles and Use Cases of StringPiece)
▮▮▮▮▮▮ 2.3.2 2.3.2 StringPiece 的常用操作与注意事项 (Common Operations and Considerations of StringPiece)
▮▮ 3. 容器与数据结构:高效的数据组织与管理 (Containers and Data Structures: Efficient Data Organization and Management)
▮▮▮▮ 3.1 3.1 FBVector:优化的动态数组 (FBVector: Optimized Dynamic Array)
▮▮▮▮▮▮ 3.1.1 3.1.1 FBVector 的内存增长策略与预分配 (Memory Growth Strategy and Pre-allocation of FBVector)
▮▮▮▮▮▮ 3.1.2 3.1.2 FBVector 的 emplace_back 与移动语义 (emplace_back and Move Semantics in FBVector)
▮▮▮▮ 3.2 3.2 F14Map 与 F14Set:高性能哈希容器 (F14Map and F14Set: High-Performance Hash Containers)
▮▮▮▮▮▮ 3.2.1 3.2.1 F14 哈希算法与性能分析 (F14 Hash Algorithm and Performance Analysis)
▮▮▮▮▮▮ 3.2.2 3.2.2 F14Map 与 F14Set 的 API 与用法 (APIs and Usage of F14Map and F14Set)
▮▮▮▮▮▮ 3.2.3 3.2.3 选择合适的哈希容器:std::unordered_map vs F14Map (Choosing the Right Hash Container: std::unordered_map vs F14Map)
▮▮▮▮ 3.3 3.3 PackedVector:节省空间的向量容器 (PackedVector: Space-Efficient Vector Container)
▮▮▮▮▮▮ 3.3.1 3.3.1 PackedVector 的位压缩原理 (Bit Packing Principle of PackedVector)
▮▮▮▮▮▮ 3.3.2 3.3.2 PackedVector 的适用场景与限制 (Use Cases and Limitations of PackedVector)
▮▮ 4. 异步编程:Futures, Promises 与协程 (Asynchronous Programming: Futures, Promises, and Coroutines)
▮▮▮▮ 4.1 4.1 Futures 与 Promises:异步结果的表示与传递 (Futures and Promises: Representing and Passing Asynchronous Results)
▮▮▮▮▮▮ 4.1.1 4.1.1 Promise 的创建与设置结果 (Creating Promises and Setting Results)
▮▮▮▮▮▮ 4.1.2 4.1.2 Future 的获取与结果等待 (Obtaining Futures and Waiting for Results)
▮▮▮▮▮▮ 4.1.3 4.1.3 Future 的链式操作与组合 (Chaining and Combining Futures)
▮▮▮▮ 4.2 4.2 Executors:任务调度与执行 (Executors: Task Scheduling and Execution)
▮▮▮▮▮▮ 4.2.1 4.2.1 Executor 接口与不同类型的 Executor (Executor Interface and Different Executor Types)
▮▮▮▮▮▮ 4.2.2 4.2.2 使用 Executor 调度异步任务 (Scheduling Asynchronous Tasks with Executors)
▮▮▮▮▮▮ 4.2.3 4.2.3 自定义 Executor 的实现 (Implementing Custom Executors)
▮▮▮▮ 4.3 4.3 协程 (Coroutines):简化异步代码的编写 (Coroutines: Simplifying Asynchronous Code Writing)
▮▮▮▮▮▮ 4.3.1 4.3.1 协程的基本概念与关键字 (Basic Concepts and Keywords of Coroutines)
▮▮▮▮▮▮ 4.3.2 4.3.2 使用协程编写异步函数 (Writing Asynchronous Functions with Coroutines)
▮▮▮▮▮▮ 4.3.3 4.3.3 协程的错误处理与取消 (Error Handling and Cancellation in Coroutines)
▮▮ 5. 网络编程:AsyncSocket 与 EventBase (Network Programming: AsyncSocket and EventBase)
▮▮▮▮ 5.1 5.1 EventBase:事件循环与 I/O 多路复用 (EventBase: Event Loop and I/O Multiplexing)
▮▮▮▮▮▮ 5.1.1 5.1.1 EventBase 的事件循环机制 (Event Loop Mechanism of EventBase)
▮▮▮▮▮▮ 5.1.2 5.1.2 使用 EventBase 监听文件描述符事件 (Listening for File Descriptor Events with EventBase)
▮▮▮▮▮▮ 5.1.3 5.1.3 EventBase 的定时器与延迟任务 (Timers and Delayed Tasks in EventBase)
▮▮▮▮ 5.2 5.2 AsyncSocket:异步套接字编程 (AsyncSocket: Asynchronous Socket Programming)
▮▮▮▮▮▮ 5.2.1 5.2.1 AsyncSocket 的创建与连接 (Creating and Connecting AsyncSockets)
▮▮▮▮▮▮ 5.2.2 5.2.2 AsyncSocket 的异步读写操作 (Asynchronous Read and Write Operations of AsyncSocket)
▮▮▮▮▮▮ 5.2.3 5.2.3 AsyncSocket 的错误处理与连接管理 (Error Handling and Connection Management in AsyncSocket)
▮▮▮▮ 5.3 5.3 简易 HTTP 服务器与客户端 (Simple HTTP Server and Client)
▮▮▮▮▮▮ 5.3.1 5.3.1 基于 AsyncSocket 构建 HTTP 服务器 (Building an HTTP Server based on AsyncSocket)
▮▮▮▮▮▮ 5.3.2 5.3.2 基于 AsyncSocket 构建 HTTP 客户端 (Building an HTTP Client based on AsyncSocket)
▮▮ 6. 并发与同步:线程、互斥锁与原子操作 (Concurrency and Synchronization: Threads, Mutexes, and Atomic Operations)
▮▮▮▮ 6.1 6.1 线程管理与线程池 (Thread Management and Thread Pools)
▮▮▮▮▮▮ 6.1.1 6.1.1 folly::Thread:线程的封装与管理 (folly::Thread: Thread Encapsulation and Management)
▮▮▮▮▮▮ 6.1.2 6.1.2 ThreadPoolExecutor:线程池的实现与配置 (ThreadPoolExecutor: Thread Pool Implementation and Configuration)
▮▮▮▮▮▮ 6.1.3 6.1.3 线程局部存储 (Thread-Local Storage)
▮▮▮▮ 6.2 6.2 互斥锁与条件变量 (Mutexes and Condition Variables)
▮▮▮▮▮▮ 6.2.1 6.2.1 folly::Mutex 与 folly::SharedMutex:互斥锁的实现 (folly::Mutex and folly::SharedMutex: Mutex Implementations)
▮▮▮▮▮▮ 6.2.2 6.2.2 folly::ConditionVariable:条件变量的实现 (folly::ConditionVariable: Condition Variable Implementation)
▮▮▮▮▮▮ 6.2.3 6.2.3 死锁的预防与避免 (Deadlock Prevention and Avoidance)
▮▮▮▮ 6.3 6.3 原子操作与无锁编程 (Atomic Operations and Lock-Free Programming)
▮▮▮▮▮▮ 6.3.1 6.3.1 folly::AtomicHashMap:原子哈希容器 (folly::AtomicHashMap: Atomic Hash Container)
▮▮▮▮▮▮ 6.3.2 6.3.2 原子操作的内存顺序与一致性 (Memory Ordering and Consistency of Atomic Operations)
▮▮▮▮▮▮ 6.3.3 6.3.3 无锁数据结构的实现案例 (Implementation Cases of Lock-Free Data Structures)
▮▮ 7. 性能优化与调试技巧 (Performance Optimization and Debugging Techniques)
▮▮▮▮ 7.1 7.1 性能分析工具与方法 (Performance Analysis Tools and Methods)
▮▮▮▮▮▮ 7.1.1 7.1.1 使用 gprof 进行性能分析 (Performance Analysis with gprof)
▮▮▮▮▮▮ 7.1.2 7.1.2 使用 perf 进行系统级性能分析 (System-Level Performance Analysis with perf)
▮▮▮▮▮▮ 7.1.3 7.1.3 火焰图 (Flame Graphs) 的生成与分析 (Generating and Analyzing Flame Graphs)
▮▮▮▮ 7.2 7.2 内存管理优化技巧 (Memory Management Optimization Techniques)
▮▮▮▮▮▮ 7.2.1 7.2.1 使用 folly::MemoryPool 优化内存分配 (Optimizing Memory Allocation with folly::MemoryPool)
▮▮▮▮▮▮ 7.2.2 7.2.2 对象池 (Object Pools) 的设计与实现 (Design and Implementation of Object Pools)
▮▮▮▮▮▮ 7.2.3 7.2.3 内存泄漏检测与预防 (Memory Leak Detection and Prevention)
▮▮▮▮ 7.3 7.3 常见调试方法与工具 (Common Debugging Methods and Tools)
▮▮▮▮▮▮ 7.3.1 7.3.1 使用 gdb 进行程序调试 (Program Debugging with gdb)
▮▮▮▮▮▮ 7.3.2 7.3.2 使用 lldb 进行程序调试 (Program Debugging with lldb)
▮▮▮▮▮▮ 7.3.3 7.3.3 Folly 提供的调试辅助工具与宏 (Debugging Utilities and Macros Provided by Folly)
▮▮ 8. Folly 库的高级主题与未来展望 (Advanced Topics and Future Prospects of Folly Library)
▮▮▮▮ 8.1 8.1 Folly 与其他库的集成 (Integration of Folly with Other Libraries)
▮▮▮▮▮▮ 8.1.1 8.1.1 Folly 与 Boost 的协同使用 (Synergistic Use of Folly and Boost)
▮▮▮▮▮▮ 8.1.2 8.1.2 Folly 与 gRPC 的集成 (Integration of Folly and gRPC)
▮▮▮▮▮▮ 8.1.3 8.1.3 Folly 与 Thrift 的集成 (Integration of Folly and Thrift)
▮▮▮▮ 8.2 8.2 扩展 Folly 库的功能 (Extending the Functionality of Folly Library)
▮▮▮▮▮▮ 8.2.1 8.2.1 贡献代码到 Folly 社区 (Contributing Code to the Folly Community)
▮▮▮▮▮▮ 8.2.2 8.2.2 添加新的 Folly 模块或组件 (Adding New Folly Modules or Components)
▮▮▮▮▮▮ 8.2.3 8.2.3 参与 Folly 社区与交流 (Participating in the Folly Community and Communication)
▮▮▮▮ 8.3 8.3 Folly 库的未来发展趋势 (Future Development Trends of Folly Library)
▮▮▮▮▮▮ 8.3.1 8.3.1 C++ 标准的演进对 Folly 的影响 (Impact of C++ Standard Evolution on Folly)
▮▮▮▮▮▮ 8.3.2 8.3.2 Folly 社区的活跃度与发展方向 (Activity and Development Direction of the Folly Community)
▮▮▮▮▮▮ 8.3.3 8.3.3 Folly 在新兴技术领域的应用前景 (Application Prospects of Folly in Emerging Technologies)
▮▮ 附录A: 附录 A:Folly 常用类和函数速查表 (Appendix A: Quick Reference for Commonly Used Folly Classes and Functions)
▮▮ 附录B: 附录 B:Folly 术语表 (Appendix B: Glossary of Folly Terms)
▮▮ 附录C: 附录 C:参考文献与扩展阅读 (Appendix C: References and Further Reading)
1. Folly 库概览:起步与核心概念 (Overview of Folly Library: Getting Started and Core Concepts)
本章作为 Folly 库的入门,将介绍 Folly 库的历史、设计哲学、核心特性以及如何快速开始使用 Folly,为后续深入学习打下基础。
1.1 什么是 Folly 库? (What is Folly Library?)
介绍 Folly 库的定义、目标和它在现代 C++ 开发中的定位,以及它与 Boost、STL 等库的区别与联系。
1.1.1 Folly 的起源与发展 (Origin and Development of Folly)
要理解 Folly 库,首先需要了解它的起源。Folly,全称 "Facebook Open-source Library",直译为 脸书开源库
,正如其名,它是由 Meta (原 Facebook) 公司开源的一个 C++ 库。它的诞生并非偶然,而是伴随着 Facebook 在其高速发展过程中,对高性能、高效率 C++ 基础设施日益增长的需求而产生的。
在 Facebook 早期,随着用户规模和业务复杂度的爆炸式增长,他们面临着许多传统 C++ 工具和标准库 (Standard Template Library, STL) 无法有效解决的问题。例如,对于超大规模分布式系统的构建,需要更强大的异步编程模型、更高效的数据结构和算法、以及更可靠的网络通信机制。为了应对这些挑战,Facebook 的工程师们开始在内部构建一系列的 C++ 组件和工具库,以满足其特定的性能和扩展性需求。
随着时间的推移,这些内部库逐渐成熟和完善,并在 Facebook 的各项核心业务中得到了广泛应用,例如社交网络平台、广告系统、消息服务等等。为了回馈开源社区,同时也为了促进 C++ 生态系统的发展,Facebook 决定将其内部一些通用且高质量的 C++ 库开源,Folly 库便应运而生。
2012 年,Folly 库正式对外开源,并在 GitHub 上发布。从那时起,Folly 库便吸引了全球众多 C++ 开发者的关注和贡献,逐渐发展成为一个活跃的开源项目。值得注意的是,Folly 库的开发和维护工作,至今仍然由 Meta 的工程师主导,这也保证了 Folly 库能够持续地吸收业界最佳实践,并保持与 Facebook 自身技术栈的同步发展。
Folly 库的开源历程,不仅体现了 Meta 公司的开源精神,也反映了现代互联网公司在技术发展上的一个趋势:将内部沉淀的优秀技术成果回馈社区,通过开源协作来共同推动技术进步。对于 C++ 开发者而言,Folly 库的出现无疑是一个福音,它提供了一个经过大规模实践检验、高性能、高质量的 C++ 工具库,可以极大地提升开发效率和程序性能。
1.1.2 Folly 的设计哲学与目标 (Design Philosophy and Goals of Folly)
Folly 库之所以能够在众多 C++ 库中脱颖而出,并被广泛应用于高性能计算、分布式系统、网络编程等领域,与其独特的设计哲学和明确的目标是密不可分的。概括来说,Folly 库的设计哲学可以总结为以下几个核心原则:
① 效率至上 (Performance First):
这是 Folly 库最核心的设计原则。Folly 库的许多组件和数据结构,都是为了追求极致的性能而设计的。例如,fbstring
旨在提供比 std::string
更高效的字符串操作,F14Map
和 F14Set
则提供了比 std::unordered_map
和 std::unordered_set
更快的哈希容器。Folly 库在设计时,会深入考虑各种性能优化的细节,例如内存管理、缓存局部性 (cache locality)、指令级并行 (instruction-level parallelism) 等,力求在各种场景下都能达到最佳的性能表现。
② 务实与实用 (Pragmatism and Practicality):
Folly 库的设计者们都是经验丰富的工程师,他们深知在实际工程项目中,理论上的完美往往难以实现,而实用性和易用性才是最重要的。因此,Folly 库在设计时,非常注重解决实际问题,提供的组件和工具都具有很强的实用价值。例如,Folly 库的异步编程框架 Futures/Promises,就很好地解决了 C++ 异步编程中回调地狱 (callback hell) 的问题,提高了异步代码的可读性和可维护性。
③ 现代 C++ 特性 (Modern C++ Features):
Folly 库紧跟 C++ 标准的发展步伐,充分利用 C++11、C++14、C++17 乃至 C++20 等新标准引入的特性,例如移动语义 (move semantics)、完美转发 (perfect forwarding)、lambda 表达式、协程 (coroutines) 等。这使得 Folly 库的代码更加简洁、高效、安全,同时也更易于理解和维护。通过采用现代 C++ 特性,Folly 库也能够更好地与现代 C++ 生态系统融合。
④ 模块化与可扩展性 (Modularity and Extensibility):
Folly 库采用了模块化的设计,将不同的功能划分为独立的模块,例如 folly::String
、folly::Container
、folly::Concurrency
、folly::IO
等。这种模块化设计使得 Folly 库的结构清晰、易于维护,同时也方便开发者按需选择和使用特定的模块,避免引入不必要的依赖。此外,Folly 库也提供了良好的扩展性,允许开发者根据自身需求扩展 Folly 库的功能,例如自定义格式化器 (formatter)、自定义执行器 (executor) 等。
⑤ 互操作性与兼容性 (Interoperability and Compatibility):
虽然 Folly 库提供了许多高性能的组件,但它并没有试图完全替代 STL 或其他 C++ 标准库。相反,Folly 库非常注重与现有 C++ 生态系统的互操作性和兼容性。例如,fbstring
可以方便地与 std::string
相互转换,Folly 库的容器也可以与 STL 算法协同工作。这种互操作性和兼容性使得开发者可以平滑地将 Folly 库引入到现有的 C++ 项目中,而无需进行大规模的代码重构。
总而言之,Folly 库的设计哲学是围绕着 高性能、实用性、现代 C++、模块化、互操作性 这几个关键词展开的。它的目标是为 C++ 开发者提供一套强大、高效、易用的工具库,以应对现代软件开发中的各种挑战,尤其是在高性能、高并发、分布式系统等领域。
1.1.3 Folly 的核心特性与模块 (Core Features and Modules of Folly)
Folly 库作为一个综合性的 C++ 工具库,包含了大量的模块和组件,涵盖了 C++ 开发的多个方面。为了帮助读者建立对 Folly 库的整体认知,本节将概览 Folly 库的主要功能模块,并简要介绍每个模块的核心特性。
① 字符串处理 (String Processing) 🔤:
folly::String
模块提供了高效的字符串处理工具,核心组件是 fbstring
。fbstring
是一种高性能的字符串实现,旨在替代 std::string
在某些场景下的不足。它采用了多种优化策略,例如小字符串优化 (Small String Optimization, SSO)、引用计数 (reference counting)、写时复制 (Copy-On-Write, COW) 等,以提高字符串操作的效率,尤其是在字符串复制、赋值等操作频繁的场景下。此外,folly::String
模块还提供了 folly::format
用于类型安全的格式化输出,以及 folly::StringPiece
用于高效的字符串视图操作。
② 容器与数据结构 (Containers and Data Structures) 📦:
folly::Container
模块提供了多种高性能的容器和数据结构,用于高效地组织和管理数据。其中包括:
⚝ FBVector
:一种优化的动态数组,旨在替代 std::vector
在某些场景下的不足,例如更高效的内存管理和预分配策略。
⚝ F14Map
和 F14Set
:高性能的哈希容器,旨在替代 std::unordered_map
和 std::unordered_set
,提供了更快的哈希算法和冲突解决策略。
⚝ PackedVector
:一种节省空间的向量容器,通过位压缩技术来存储小整数,适用于需要存储大量小整数且内存受限的场景。
⚝ ConcurrentHashMap
和 ConcurrentSkipListMap
:并发哈希map和并发跳表map,用于在多线程环境下安全地访问和修改共享数据。
③ 异步编程 (Asynchronous Programming) 🚀:
folly::Futures
模块是 Folly 库中用于异步编程的核心模块,提供了 Futures 和 Promises 机制,用于表示和传递异步操作的结果。Futures/Promises 是一种强大的异步编程模型,可以有效地解决回调地狱问题,提高异步代码的可读性和可维护性。此外,folly::Futures
模块还提供了 folly::Executor
框架,用于任务调度和执行,支持多种类型的执行器,例如线程池执行器 (ThreadPoolExecutor)、内联执行器 (InlineExecutor) 等。Folly 库还支持协程 (Coroutines),通过 co_await
, co_return
等关键字,可以更加简洁地编写异步代码。
④ 网络编程 (Network Programming) 🌐:
folly::IO
模块提供了用于网络编程的组件,核心组件是 EventBase
和 AsyncSocket
。EventBase
是 Folly 库的事件循环 (event loop) 核心,基于 I/O 多路复用 (I/O multiplexing) 技术 (例如 epoll, kqueue) 实现,用于高效地处理 I/O 事件。AsyncSocket
则是基于 EventBase
的异步套接字 (asynchronous socket) 封装,提供了非阻塞的网络通信接口。通过 EventBase
和 AsyncSocket
,开发者可以构建高性能、可扩展的网络应用,例如网络服务器、客户端等。
⑤ 并发与同步 (Concurrency and Synchronization) 🧵:
folly::Concurrency
模块提供了多种并发与同步工具,用于构建线程安全、高效的并发程序。其中包括:
⚝ folly::Thread
:线程的封装和管理工具,简化了线程的创建和管理操作。
⚝ ThreadPoolExecutor
:线程池的实现,用于高效地管理和复用线程资源。
⚝ folly::Mutex
和 folly::SharedMutex
:互斥锁的实现,用于保护共享资源,防止数据竞争 (data race)。
⚝ folly::ConditionVariable
:条件变量的实现,用于线程间的同步和通信。
⚝ 原子操作 (atomic operations) 相关工具,例如 folly::AtomicHashMap
,用于实现无锁数据结构和算法。
⑥ 时间与定时器 (Time and Timers) ⏱️:
folly::Time
模块提供了时间和定时器相关的工具,用于处理时间相关的操作,例如获取当前时间、时间格式化、定时任务等。folly::Timer
类提供了基于 EventBase
的定时器功能,可以方便地在事件循环中注册定时任务。
⑦ 配置与选项 (Configuration and Options) ⚙️:
folly::Options
模块提供了用于处理程序配置和选项的工具,可以方便地解析命令行参数、配置文件等,并将配置信息传递给程序的各个组件。
⑧ 实用工具 (Utilities) 🛠️:
folly::Utility
模块包含了各种实用的工具函数和类,例如类型 traits (type traits)、断言 (assertions)、日志 (logging)、JSON 解析 (JSON parsing) 等,这些工具可以辅助开发者进行日常开发工作,提高开发效率。
除了以上列举的主要模块,Folly 库还包含许多其他有用的模块和组件,例如 folly::Benchmark
用于性能基准测试,folly::Memory
用于内存管理,folly::Hash
提供了多种哈希算法的实现等等。读者可以通过阅读 Folly 库的官方文档和源代码,深入了解各个模块的详细功能和用法。
总而言之,Folly 库是一个功能丰富、模块齐全的 C++ 工具库,涵盖了字符串处理、容器、异步编程、网络编程、并发与同步、时间与定时器、配置与选项、实用工具等多个方面,为 C++ 开发者提供了强大的支持,可以应用于各种高性能、高并发的应用场景。
1.2 快速上手:环境搭建与编译 (Quick Start: Environment Setup and Compilation)
指导读者如何搭建 Folly 库的开发环境,包括依赖项安装、编译配置和示例代码运行,帮助读者快速开始实践。
1.2.1 依赖项安装与管理 (Dependency Installation and Management)
在开始使用 Folly 库之前,需要先搭建好开发环境,并安装 Folly 库的依赖项 (dependencies)。Folly 库依赖于一些其他的开源库,这些库提供了 Folly 库正常运行所必需的功能。以下是 Folly 库的主要依赖项:
① Boost 库:
Boost 库 (https://www.boost.org/) 是一个非常流行的 C++ 准标准库,提供了大量的通用 C++ 组件,被广泛应用于各种 C++ 项目中。Folly 库的很多模块都依赖于 Boost 库,例如 Boost.Context, Boost.Coroutine2, Boost.Optional, Boost.Range, Boost.Regex, Boost.System, Boost.Thread 等。因此,安装 Boost 库是使用 Folly 库的前提条件。
② Double-conversion 库:
Double-conversion 库 (https://github.com/google/double-conversion) 是 Google 开源的一个高效的 double 和 string 相互转换的库。Folly 库在字符串处理和数值转换方面使用了 double-conversion 库,以提高性能和精度。
③ Glog 库:
Glog 库 (https://github.com/google/glog) 也是 Google 开源的一个日志库,提供了高性能的日志记录功能。Folly 库使用 glog 库进行日志输出。
④ Gflags 库:
Gflags 库 (https://gflags.github.io/gflags/) 是 Google 开源的命令行参数解析库。Folly 库使用 gflags 库来处理命令行参数。
⑤ Libevent 库:
Libevent 库 (http://libevent.org/) 是一个事件通知库,常用于网络编程,支持多种 I/O 多路复用技术 (例如 epoll, kqueue)。Folly 库的 EventBase
组件基于 libevent 库实现。
⑥ LZ4 库:
LZ4 库 (https://lz4.github.io/lz4/) 是一种快速的无损压缩算法。Folly 库在某些模块中使用了 LZ4 库进行数据压缩。
⑦ OpenSSL 库:
OpenSSL 库 (https://www.openssl.org/) 是一个强大的安全套接字层协议 (Secure Sockets Layer, SSL) 和传输层安全协议 (Transport Layer Security, TLS) 库,也提供了通用的加密算法和工具。Folly 库在网络编程和安全通信方面使用了 OpenSSL 库。
⑧ Zlib 库:
Zlib 库 (https://zlib.net/) 是一个通用的数据压缩库,提供了 DEFLATE 压缩算法的实现。Folly 库在某些模块中使用了 zlib 库进行数据压缩。
⑨ fmt 库:
fmt 库 (https://fmt.dev/) 是一个现代的 C++ 格式化库,提供了类型安全且可扩展的格式化输出功能。Folly 库的 folly::format
组件在底层使用了 fmt 库。
⑩ Libsodium 库 (可选):
Libsodium 库 (https://libsodium.org/) 是一个现代的、易于使用的加密库。Folly 库在某些安全相关的模块中可选地使用了 libsodium 库。
⑪ Brotli 库 (可选):
Brotli 库 (https://github.com/google/brotli) 是 Google 开源的一种通用的无损数据压缩算法。Folly 库在某些模块中可选地使用了 Brotli 库进行数据压缩。
依赖项安装方法
安装 Folly 库的依赖项,通常有两种方法:使用包管理器 (package manager) 或手动编译安装。
⚝ 使用包管理器:
对于大多数 Linux 发行版和 macOS 系统,可以使用系统自带的包管理器来安装 Folly 库的依赖项。例如,在 Ubuntu/Debian 系统上,可以使用 apt
命令:
1
sudo apt update
2
sudo apt install libboost-dev libdouble-conversion-dev libgflags-dev libglog-dev libevent-dev liblz4-dev libssl-dev zlib1g-dev libfmt-dev libsodium-dev libbrotli-dev cmake g++
在 macOS 系统上,如果安装了 Homebrew 包管理器,可以使用 brew
命令:
1
brew update
2
brew install boost double-conversion gflags glog libevent lz4 openssl zlib fmt libsodium brotli cmake
使用包管理器安装依赖项的优点是简单快捷,缺点是可能安装的版本不是最新的,或者与 Folly 库要求的版本不完全匹配。
⚝ 手动编译安装:
如果需要使用特定版本的依赖项,或者包管理器提供的版本不满足需求,可以选择手动编译安装依赖项。手动编译安装的步骤通常包括:
1. 下载依赖库的源代码。
2. 使用 CMake 或 Autotools 等构建工具配置和编译依赖库。
3. 将编译生成的库文件和头文件安装到系统目录或自定义目录。
手动编译安装依赖项的优点是可以灵活控制版本和安装路径,缺点是过程相对复杂,需要一定的编译和构建知识。
依赖项管理建议
为了更好地管理 Folly 库的依赖项,建议采用以下策略:
⚝ 使用 CMake 管理依赖:
Folly 库本身使用 CMake 作为构建系统,因此建议在基于 Folly 库的项目中也使用 CMake 进行构建和依赖管理。CMake 提供了 find_package
命令,可以方便地查找和链接依赖库。
⚝ 使用 vcpkg 或 Conan 等 C++ 包管理器:
vcpkg (https://github.com/microsoft/vcpkg) 和 Conan (https://conan.io/) 是流行的 C++ 包管理器,可以自动化地下载、编译和安装 C++ 依赖库。使用 vcpkg 或 Conan 可以简化 Folly 库依赖项的管理过程。
⚝ 保持依赖项版本与 Folly 库版本兼容:
Folly 库的不同版本可能对依赖项的版本有不同的要求。在安装依赖项时,务必参考 Folly 库的官方文档或构建指南,确保安装的依赖项版本与 Folly 库版本兼容。
1.2.2 使用 CMake 构建 Folly 项目 (Building Folly Projects with CMake)
CMake (Cross-platform Make) 是一个跨平台的构建系统,用于管理软件的构建过程。Folly 库本身就是使用 CMake 构建的,因此使用 CMake 构建基于 Folly 库的项目是最佳实践。本节将讲解如何使用 CMake 构建 Folly 项目,包括 CMakeLists.txt
的配置和编译命令。
CMakeLists.txt 配置
在基于 Folly 库的项目中,需要在项目根目录下创建一个 CMakeLists.txt
文件,用于描述项目的构建规则和依赖关系。一个基本的 CMakeLists.txt
文件通常包含以下内容:
1
cmake_minimum_required(VERSION 3.10) # 指定 CMake 最低版本
2
project(MyFollyProject) # 项目名称
3
4
find_package(Folly REQUIRED) # 查找 Folly 库
5
6
add_executable(my_folly_app main.cpp) # 添加可执行文件
7
8
target_link_libraries(my_folly_app PRIVATE Folly::folly) # 链接 Folly 库
⚝ cmake_minimum_required(VERSION 3.10)
:指定 CMake 的最低版本要求,确保使用的 CMake 版本不低于 3.10。
⚝ project(MyFollyProject)
:定义项目名称为 MyFollyProject
,可以根据实际项目名称修改。
⚝ find_package(Folly REQUIRED)
:使用 CMake 的 find_package
命令查找 Folly 库。REQUIRED
关键字表示如果找不到 Folly 库,CMake 将会报错并停止配置过程。CMake 会在系统默认路径、环境变量指定的路径、以及 CMake 模块路径中查找 Folly 库的配置文件 FollyConfig.cmake
或 folly-config.cmake
,从而找到 Folly 库的安装路径和编译选项。
⚝ add_executable(my_folly_app main.cpp)
:使用 add_executable
命令添加一个可执行文件目标,目标名称为 my_folly_app
,源文件为 main.cpp
。main.cpp
是项目的主程序入口文件,需要根据实际情况创建。
⚝ target_link_libraries(my_folly_app PRIVATE Folly::folly)
:使用 target_link_libraries
命令将可执行文件 my_folly_app
链接到 Folly 库。PRIVATE
关键字表示 Folly 库是 my_folly_app
的私有依赖,不会传递给其他依赖于 my_folly_app
的目标。Folly::folly
是 Folly 库在 CMake 中注册的目标名称,通过 find_package(Folly)
命令找到。
CMake 构建步骤
配置好 CMakeLists.txt
文件后,就可以使用 CMake 进行项目构建。CMake 构建过程通常包括以下步骤:
- 创建构建目录:
在项目根目录下创建一个构建目录,例如build
:
1
mkdir build
2
cd build
推荐在项目根目录外创建构建目录,例如 my-folly-project-build
,避免污染源代码目录。
- 运行 CMake 配置:
在构建目录中运行cmake
命令,指定源代码路径。源代码路径通常是项目根目录的上一级目录..
:
1
cmake ..
CMake 会根据 CMakeLists.txt
文件,查找依赖库、生成构建系统所需的 Makefile 或 Ninja 文件。CMake 配置过程会输出详细的配置信息,包括找到的依赖库、编译选项等。如果配置过程中出现错误,需要检查 CMakeLists.txt
文件和依赖项安装是否正确。
- 运行编译命令:
配置成功后,在构建目录中运行编译命令,例如make
或ninja
:
1
make # 或 ninja
make
命令会调用 Makefile 文件进行编译,ninja
命令会调用 Ninja 文件进行编译。Ninja 通常比 Make 更快,尤其是在大型项目中。编译过程会将源代码编译成可执行文件或库文件。
- 运行安装命令 (可选):
如果需要在系统范围内安装编译生成的可执行文件或库文件,可以运行安装命令:
1
sudo make install # 或 sudo ninja install
安装命令会将可执行文件复制到 /usr/local/bin
目录,库文件复制到 /usr/local/lib
目录,头文件复制到 /usr/local/include
目录。安装是可选步骤,如果只是在本地运行程序,可以跳过安装步骤。
编译选项配置
CMake 允许通过命令行选项或在 CMakeLists.txt
文件中配置编译选项,例如编译类型 (Debug/Release)、编译器类型、优化级别等。常用的 CMake 编译选项包括:
⚝ -DCMAKE_BUILD_TYPE=Debug
或 -DCMAKE_BUILD_TYPE=Release
:指定编译类型为 Debug 或 Release。Debug 类型包含调试信息,Release 类型进行优化。
⚝ -DCMAKE_CXX_COMPILER=/path/to/g++
:指定 C++ 编译器路径。
⚝ -DCMAKE_INSTALL_PREFIX=/path/to/install/dir
:指定安装目录。
可以在运行 cmake
命令时,通过 -D
选项传递编译选项,例如:
1
cmake -DCMAKE_BUILD_TYPE=Release ..
也可以在 CMakeLists.txt
文件中使用 set
命令设置编译选项,例如:
1
set(CMAKE_BUILD_TYPE Release)
1.2.3 运行第一个 Folly 程序 (Running Your First Folly Program)
为了验证 Folly 库的环境配置和编译是否正确,本节将提供一个简单的 Folly 示例程序,并指导读者编译和运行该程序。
示例代码
创建一个名为 main.cpp
的文件,并将以下代码复制到文件中:
1
#include <folly/FBString.h>
2
#include <iostream>
3
4
int main() {
5
folly::fbstring message = "Hello, Folly!";
6
std::cout << message << std::endl;
7
return 0;
8
}
这个示例程序非常简单,它使用了 Folly 库的 fbstring
组件,创建了一个 folly::fbstring
对象,并将其输出到标准输出。
编译和运行
- 创建
CMakeLists.txt
文件:
在main.cpp
文件所在目录下创建CMakeLists.txt
文件,并将以下内容复制到文件中:
1
cmake_minimum_required(VERSION 3.10)
2
project(HelloFolly)
3
find_package(Folly REQUIRED)
4
add_executable(hello_folly main.cpp)
5
target_link_libraries(hello_folly PRIVATE Folly::folly)
- 创建构建目录并配置:
在main.cpp
文件所在目录下创建build
目录,并进入build
目录,运行cmake ..
命令进行配置:
1
mkdir build
2
cd build
3
cmake ..
- 编译程序:
在build
目录中运行make
命令进行编译:
1
make
编译成功后,会在 build
目录中生成可执行文件 hello_folly
(或 hello_folly.exe
在 Windows 系统上)。
- 运行程序:
在build
目录中运行可执行文件hello_folly
:
1
./hello_folly # 或在 Windows 上运行 hello_folly.exe
如果环境配置和编译都正确,程序将会在终端输出 Hello, Folly!
。
如果程序能够成功输出 Hello, Folly!
,则表明 Folly 库的环境搭建和编译配置是正确的,可以开始进一步学习和使用 Folly 库的其他功能。如果编译或运行过程中出现错误,需要仔细检查错误信息,并根据错误信息排查问题,例如检查依赖项是否安装正确、CMake 配置是否正确、代码是否存在语法错误等。
1.3 Folly 的命名空间与代码组织 (Folly Namespaces and Code Organization)
介绍 Folly 库的命名空间结构和代码组织方式,帮助读者理解 Folly 的模块划分和代码查找。
1.3.1 顶层命名空间 folly (Top-level Namespace folly)
在 C++ 中,命名空间 (namespace) 用于组织代码,避免命名冲突。Folly 库的所有公开接口 (public interface) 都定义在 folly
命名空间 (top-level namespace) 下。这意味着,在使用 Folly 库的任何组件时,都需要显式地或隐式地使用 folly
命名空间。
显式使用命名空间
最常见的用法是显式地在代码中使用 folly::
前缀来访问 Folly 库的组件,例如:
1
#include <folly/FBString.h>
2
#include <iostream>
3
4
int main() {
5
folly::fbstring message = "Hello, Folly!"; // 显式使用 folly::fbstring
6
std::cout << message << std::endl;
7
return 0;
8
}
在上面的代码中,folly::fbstring
明确地指定了 fbstring
类型来自 folly
命名空间。这种方式的优点是代码清晰易懂,明确地表明了使用的组件来自 Folly 库,避免了命名冲突的可能性。缺点是代码略显冗长,每次使用 Folly 组件都需要添加 folly::
前缀。
隐式使用命名空间
为了简化代码,可以使用 using namespace folly;
语句将 folly
命名空间引入到当前作用域 (scope) 中,例如:
1
#include <folly/FBString.h>
2
#include <iostream>
3
4
using namespace folly; // 引入 folly 命名空间
5
6
int main() {
7
fbstring message = "Hello, Folly!"; // 隐式使用 fbstring
8
std::cout << message << std::endl;
9
return 0;
10
}
在上面的代码中,using namespace folly;
语句将 folly
命名空间中的所有名称 (names) 引入到全局命名空间中。之后,就可以直接使用 fbstring
而无需添加 folly::
前缀。这种方式的优点是代码简洁,缺点是可能会引入命名冲突的风险,尤其是在大型项目中,不同库之间可能存在相同的名称。
子命名空间 (Sub-namespaces)
为了更好地组织代码,Folly 库在 folly
命名空间下进一步划分了多个子命名空间 (sub-namespaces),每个子命名空间对应一个功能模块,例如:
⚝ folly::string
:字符串处理模块,包含 fbstring
, format
, StringPiece
等组件。
⚝ folly::container
:容器与数据结构模块,包含 FBVector
, F14Map
, F14Set
等组件。
⚝ folly::futures
:异步编程模块,包含 Futures, Promises, Executor 等组件。
⚝ folly::io
:网络编程模块,包含 EventBase
, AsyncSocket
等组件。
⚝ folly::concurrency
:并发与同步模块,包含 Thread
, Mutex
, AtomicHashMap
等组件。
⚝ folly::time
:时间与定时器模块。
⚝ folly::options
:配置与选项模块。
⚝ folly::json
:JSON 处理模块。
⚝ folly::hash
:哈希算法模块。
⚝ folly::memory
:内存管理模块。
⚝ folly::logging
:日志模块。
⚝ folly::test
:测试框架模块。
⚝ folly::detail
:内部实现细节模块 (通常不对外公开使用)。
使用子命名空间可以更加精细地组织代码,避免不同模块之间的命名冲突。在代码中,可以使用完整的命名空间路径来访问特定模块的组件,例如 folly::string::fbstring
, folly::container::FBVector
, folly::futures::Future
等。
命名空间使用建议
在实际项目开发中,关于命名空间的使用,建议遵循以下原则:
⚝ 在头文件中避免使用 using namespace
:
在头文件中使用 using namespace
会将命名空间引入到全局作用域,可能会影响到包含该头文件的其他源文件,导致意想不到的命名冲突。因此,在头文件中应该避免使用 using namespace
,而是显式地使用完整的命名空间路径。
⚝ 在源文件中谨慎使用 using namespace
:
在源文件中可以使用 using namespace
来简化代码,但需要谨慎使用,避免引入不必要的命名冲突。如果只使用 Folly 库的少量组件,可以考虑只引入特定的名称,例如 using folly::fbstring;
, using folly::format;
,而不是直接 using namespace folly;
。
⚝ 优先使用显式命名空间访问:
在代码中,优先使用显式的命名空间访问方式 (例如 folly::fbstring
),可以提高代码的可读性和可维护性,减少命名冲突的风险。只有在代码块中频繁使用某个命名空间的组件时,才考虑使用 using namespace
或 using
声明来简化代码。
1.3.2 模块化设计与头文件组织 (Modular Design and Header File Organization)
Folly 库采用了模块化设计,将不同的功能划分为独立的模块。这种模块化设计不仅体现在命名空间划分上,也体现在头文件 (header file) 的组织方式上。Folly 库的头文件按照模块进行组织,每个模块通常对应一个或多个子目录,并在子目录中包含相关的头文件。
头文件目录结构
Folly 库的头文件目录结构大致如下 (以 GitHub 仓库中的 folly/
目录为例):
1
folly/
2
├── AtomicHashMap.h
3
├── AtomicQueue.h
4
├── AtomicStack.h
5
├── Benchmark.h
6
├── Blogging.h
7
├── CPUThreadPoolExecutor.h
8
├── CallOnce.h
9
├── ConcurrentHashMap.h
10
├── ConcurrentSkipListMap.h
11
├── Container.h
12
├── Conv.h
13
├── Coro.h
14
├── Demangle.h
15
├── DynamicConverter.h
16
├── Executor.h
17
├── FBString.h
18
├── F14Map.h
19
├── F14Set.h
20
├── Format.h
21
├── Futures.h
22
├── Hash.h
23
├── IOBuf.h
24
├── IOQueueThreadPoolExecutor.h
25
├── Init.h
26
├── IntrusiveList.h
27
├──json/
28
│ ├── Dynamic.h
29
│ ├── DynamicConverter.h
30
│ ├── json.h
31
│ ├── json_pointer.h
32
│ ├── json_spirit.h
33
│ └── detail/
34
├── logging/
35
│ ├── ...
36
├── memory/
37
│ ├── ...
38
├── options/
39
│ ├── ...
40
├── portablehash/
41
│ ├── ...
42
├── profiler/
43
│ ├── ...
44
├── random/
45
│ ├── ...
46
├── stats/
47
│ ├── ...
48
├── string/
49
│ ├── Ascii.h
50
│ ├── Compression.h
51
│ ├── FBString.h
52
│ ├── Format.h
53
│ ├── String.h
54
│ ├── StringPiece.h
55
│ ├── detail/
56
│ └── fbstring-inl.h
57
├── test/
58
│ ├── ...
59
├── threading/
60
│ ├── ...
61
├── time/
62
│ ├── ...
63
├── utility/
64
│ ├── ...
65
└── wangle/
66
├── ...
可以看到,Folly 库的头文件按照功能模块组织在不同的子目录中,例如 string/
目录包含了字符串处理模块的头文件,container/
目录包含了容器与数据结构模块的头文件,futures/
目录包含了异步编程模块的头文件,以此类推。
按需包含头文件
在使用 Folly 库的某个模块时,只需要包含该模块对应的头文件即可,无需包含整个 Folly 库的所有头文件。例如,如果只需要使用 fbstring
组件,只需要包含 <folly/FBString.h>
头文件即可:
1
#include <folly/FBString.h> // 只需包含 FBString.h
2
3
int main() {
4
folly::fbstring message = "Hello, Folly!";
5
// ...
6
return 0;
7
}
这种按需包含头文件的方式,可以减少编译依赖,缩短编译时间,并提高代码的可读性。
模块化优势
Folly 库的模块化设计和头文件组织方式,带来了以下优势:
⚝ 降低编译依赖:
按需包含头文件可以减少编译依赖,只编译需要的模块,缩短编译时间。
⚝ 提高代码可读性:
模块化的目录结构和头文件组织方式,使得代码结构清晰,易于理解和维护。开发者可以根据模块名称快速找到需要的组件和头文件。
⚝ 方便模块化使用:
开发者可以根据项目需求,选择性地使用 Folly 库的特定模块,无需引入整个库,降低了项目的依赖复杂度。
⚝ 促进代码复用:
模块化的设计使得 Folly 库的各个模块可以独立地被其他项目复用,提高了代码的复用率。
1.3.3 代码风格与约定 (Code Style and Conventions)
为了保持代码的一致性和可读性,Folly 库遵循一定的代码风格规范 (code style guide) 和约定 (conventions)。了解 Folly 库的代码风格和约定,有助于读者更好地阅读和理解 Folly 库的源代码,也有助于开发者在贡献代码时遵循 Folly 社区的规范。
代码风格规范
Folly 库的代码风格规范主要参考 Google C++ Style Guide (https://google.github.io/styleguide/cppguide.html),并在此基础上进行了一些定制和扩展。Folly 库的代码风格规范主要包括以下方面:
① 代码格式化 (Formatting):
⚝ 缩进:使用 2 个空格进行缩进,不使用 Tab 字符。
⚝ 行长度:每行代码长度不超过 80 个字符,特殊情况下可以适当放宽限制。
⚝ 空格:在运算符、逗号、分号等符号周围添加适当的空格,提高代码可读性。
⚝ 空行:在函数、类、命名空间等代码块之间添加空行,分隔代码逻辑。
⚝ 换行:在长语句、长表达式、长函数参数列表等处进行适当的换行,保持代码整洁。
② 命名约定 (Naming Conventions):
⚝ 变量名:使用小写字母和下划线组合 (snake_case),例如 variable_name
。
⚝ 函数名:使用驼峰命名法 (CamelCase),首字母大写,例如 FunctionName
。
⚝ 类名:使用驼峰命名法 (CamelCase),首字母大写,例如 ClassName
。
⚝ 命名空间名:使用小写字母,例如 folly
, string
, container
。
⚝ 宏定义:使用全大写字母和下划线组合 (UPPER_SNAKE_CASE),例如 MAX_VALUE
。
⚝ 枚举类型名:使用驼峰命名法 (CamelCase),首字母大写,例如 EnumType
。
⚝ 枚举值名:使用全大写字母和下划线组合 (UPPER_SNAKE_CASE),例如 ENUM_VALUE
。
⚝ 模板参数名:使用单个大写字母,例如 T
, U
, V
。
③ 注释 (Comments):
⚝ 行注释:使用 //
进行行注释,用于注释单行代码或简短的代码片段。
⚝ 块注释:使用 /* ... */
进行块注释,用于注释多行代码或详细的代码说明。
⚝ 文档注释:对于公开接口 (public interface),使用 Doxygen 风格的文档注释,生成 API 文档。
⚝ TODO 注释:使用 TODO
标记待办事项,方便后续查找和处理。
④ 其他约定:
⚝ 头文件包含:使用 #include <folly/xxx.h>
包含 Folly 库的头文件,使用 #include <iostream>
包含标准库头文件,使用 #include "xxx.h"
包含项目本地头文件。头文件包含顺序通常为:项目本地头文件、C++ 标准库头文件、第三方库头文件、Folly 库头文件。
⚝ 断言 (Assertions):使用 CHECK(condition)
宏进行断言检查,用于在开发和调试阶段检测程序错误。在 Release 版本中,断言会被禁用。
⚝ 日志 (Logging):使用 LOG(severity) << message;
宏进行日志输出,用于记录程序运行时的信息。Folly 库使用 glog 库进行日志输出。
⚝ 异常处理 (Exception Handling):谨慎使用异常处理,避免过度使用异常。对于可预期的错误,优先使用错误码 (error code) 或返回值 (return value) 进行处理。对于不可预期的严重错误,可以使用异常进行处理。
代码风格检查工具
为了帮助开发者遵循 Folly 库的代码风格规范,Folly 社区提供了一些代码风格检查工具,例如:
⚝ Clang-Format:Clang-Format (https://clang.llvm.org/docs/ClangFormat.html) 是一个代码格式化工具,可以自动格式化 C++ 代码,使其符合指定的代码风格规范。Folly 库的 CI (持续集成) 系统使用 Clang-Format 进行代码格式化检查。开发者可以使用 Clang-Format 格式化自己的代码,使其符合 Folly 库的代码风格。
⚝ Clang-Tidy:Clang-Tidy (https://clang.llvm.org/extra/clang-tidy/) 是一个代码静态分析工具,可以检查 C++ 代码中的潜在错误和风格问题。Folly 库的 CI 系统也使用 Clang-Tidy 进行代码静态分析。开发者可以使用 Clang-Tidy 检查自己的代码,发现潜在的问题并改进代码质量。
通过遵循 Folly 库的代码风格规范和约定,并使用代码风格检查工具,可以提高代码的可读性、可维护性,并与 Folly 社区的代码风格保持一致。这对于参与 Folly 库的开发和贡献代码是非常重要的。
2. 字符串处理:强大的 folly::fbstring 与格式化 (String Processing: Powerful folly::fbstring and Formatting)
本章深入探讨 Folly 库中用于字符串处理的核心组件,包括 fbstring
的高效实现和灵活的字符串格式化工具,帮助读者掌握高性能字符串操作技巧。
2.1 folly::fbstring:高效的字符串实现 (folly::fbstring: Efficient String Implementation)
folly::fbstring
是 Folly 库提供的,用于替代 std::string
的高效字符串实现。它在设计上考虑了性能和内存效率,特别是在高并发和大规模字符串操作的场景下,fbstring
往往能展现出比 std::string
更优的表现。本节将详细介绍 fbstring
的设计原理、内存管理策略和性能优势,并与 std::string
进行对比,分析其适用场景。
2.1.1 fbstring 的内存管理与优化 (Memory Management and Optimization of fbstring)
fbstring
为了实现高效的字符串操作,在内存管理上采用了多种优化策略,主要包括小字符串优化 (Small String Optimization, SSO)、引用计数 (Reference Counting) 和写时复制 (Copy-On-Write, COW) 等机制。
① 小字符串优化 (SSO):
▮▮▮▮对于较短的字符串,fbstring
会直接在栈上或对象内部的固定大小缓冲区中存储字符串数据,而无需动态分配堆内存。这种优化避免了堆内存分配和释放的开销,提高了小字符串操作的效率。
▮▮▮▮例如,当创建一个长度小于一定阈值(例如 23 字节)的 fbstring
时,字符串的内容会被直接存储在 fbstring
对象自身的内存空间中。只有当字符串长度超过这个阈值时,fbstring
才会动态分配堆内存来存储字符串数据。
▮▮▮▮```cpp
▮▮▮▮#include
▮▮▮▮#include
▮▮▮▮int main() {
▮▮▮▮ folly::fbstring short_str = "short string"; // 可能使用 SSO
▮▮▮▮ folly::fbstring long_str = "This is a very long string that exceeds the SSO threshold."; // 需要堆内存分配
▮▮▮▮ std::cout << "short_str capacity: " << short_str.capacity() << std::endl; // 容量可能小于字符串长度,因为使用了 SSO 内部缓冲区
▮▮▮▮ std::cout << "long_str capacity: " << long_str.capacity() << std::endl; // 容量通常大于等于字符串长度,因为使用了堆内存
▮▮▮▮ return 0;
▮▮▮▮}
▮▮▮▮```
② 引用计数 (Reference Counting):
▮▮▮▮当多个 fbstring
对象共享同一个字符串数据时,fbstring
使用引用计数来跟踪有多少个对象正在共享这份数据。只有当最后一个引用消失时,才会释放字符串数据所占用的内存。
▮▮▮▮引用计数可以有效地减少内存复制和分配的次数,尤其是在字符串拷贝频繁的场景下。例如,当将一个 fbstring
对象赋值给另一个 fbstring
对象时,并不会发生实际的字符串数据拷贝,而是仅仅增加引用计数。
③ 写时复制 (COW):
▮▮▮▮写时复制是一种延迟复制的优化技术。当多个 fbstring
对象共享同一个字符串数据时,如果其中一个对象需要修改字符串内容,fbstring
才会真正地进行数据复制,为修改对象创建一个新的字符串副本。
▮▮▮▮在修改操作发生之前,所有的 fbstring
对象仍然共享同一份数据,从而避免了不必要的复制开销。只有在真正需要修改数据时,才会进行复制操作。
▮▮▮▮需要注意的是,现代 C++ 标准 (C++11 及以后) 中,std::string
的实现通常不再强制要求使用 COW,因为在多线程环境下,COW 可能会引入额外的同步开销,反而降低性能。folly::fbstring
的 COW 实现也需要考虑线程安全问题,并进行相应的同步控制。
总而言之,fbstring
通过 SSO、引用计数和 COW 等内存管理机制,在保证字符串操作效率的同时,尽可能地减少内存分配和复制的开销,尤其是在处理大量小字符串和字符串拷贝操作时,能够显著提升性能。
2.1.2 fbstring 的常用操作与接口 (Common Operations and Interfaces of fbstring)
folly::fbstring
提供了丰富的成员函数和操作符,用于字符串的构造、赋值、查找、修改和比较等操作。这些接口与 std::string
的接口设计非常相似,方便开发者从 std::string
迁移到 fbstring
。
① 构造与赋值:
▮▮▮▮fbstring
提供了多种构造函数,可以从 C 风格字符串、std::string
、或其他 fbstring
对象构造新的 fbstring
对象。同时也支持拷贝赋值和移动赋值操作。
1
#include <folly/FBString.h>
2
#include <string>
3
#include <iostream>
4
5
int main() {
6
// 默认构造函数
7
folly::fbstring fb_str1;
8
9
// 从 C 风格字符串构造
10
folly::fbstring fb_str2("hello");
11
12
// 从 std::string 构造
13
std::string std_str = "world";
14
folly::fbstring fb_str3(std_str);
15
16
// 拷贝构造
17
folly::fbstring fb_str4 = fb_str2;
18
19
// 移动构造
20
folly::fbstring fb_str5 = std::move(fb_str4); // fb_str4 的内容会被移动到 fb_str5,fb_str4 变为空
21
22
// 赋值操作
23
fb_str1 = "assignment";
24
fb_str1 = std_str;
25
fb_str1 = fb_str2;
26
27
std::cout << "fb_str1: " << fb_str1.c_str() << std::endl;
28
std::cout << "fb_str2: " << fb_str2.c_str() << std::endl;
29
std::cout << "fb_str3: " << fb_str3.c_str() << std::endl;
30
std::cout << "fb_str5: " << fb_str5.c_str() << std::endl;
31
32
return 0;
33
}
② 查找操作:
▮▮▮▮fbstring
提供了 find
、rfind
、find_first_of
、find_first_not_of
、find_last_of
、find_last_not_of
等成员函数,用于在字符串中查找子串或字符。这些函数的用法和行为与 std::string
相应函数基本一致。
1
#include <folly/FBString.h>
2
#include <iostream>
3
4
int main() {
5
folly::fbstring fb_str = "hello world hello";
6
7
// 查找子串
8
size_t pos1 = fb_str.find("world");
9
if (pos1 != folly::fbstring::npos) {
10
std::cout << "Found 'world' at position: " << pos1 << std::endl; // Output: Found 'world' at position: 6
11
}
12
13
// 从指定位置开始查找
14
size_t pos2 = fb_str.find("hello", 1); // 从索引 1 开始查找 "hello"
15
if (pos2 != folly::fbstring::npos) {
16
std::cout << "Found 'hello' starting from position 1 at position: " << pos2 << std::endl; // Output: Found 'hello' starting from position 1 at position: 12
17
}
18
19
// 未找到子串
20
size_t pos3 = fb_str.find("foobar");
21
if (pos3 == folly::fbstring::npos) {
22
std::cout << "'foobar' not found" << std::endl; // Output: 'foobar' not found
23
}
24
25
return 0;
26
}
③ 修改操作:
▮▮▮▮fbstring
提供了 append
、push_back
、insert
、erase
、replace
等成员函数,用于修改字符串的内容,例如追加、插入、删除和替换子串。这些修改操作会触发写时复制 (COW) 机制(如果适用),确保修改操作不会影响到其他共享同一份数据的 fbstring
对象。
1
#include <folly/FBString.h>
2
#include <iostream>
3
4
int main() {
5
folly::fbstring fb_str = "hello";
6
7
// 追加字符串
8
fb_str.append(" world");
9
std::cout << "After append: " << fb_str.c_str() << std::endl; // Output: After append: hello world
10
11
// 插入字符串
12
fb_str.insert(5, ",");
13
std::cout << "After insert: " << fb_str.c_str() << std::endl; // Output: After insert: hello, world
14
15
// 删除子串
16
fb_str.erase(5, 1); // 从索引 5 开始删除 1 个字符
17
std::cout << "After erase: " << fb_str.c_str() << std::endl; // Output: After erase: hello world
18
19
// 替换子串
20
fb_str.replace(6, 5, "Folly"); // 从索引 6 开始替换 5 个字符为 "Folly"
21
std::cout << "After replace: " << fb_str.c_str() << std::endl; // Output: After replace: hello Folly
22
23
return 0;
24
}
④ 与其他 std::string
相似的操作:
▮▮▮▮fbstring
还提供了许多与 std::string
相似的接口,例如:
▮▮▮▮⚝ operator[]
和 at()
:访问字符串中的字符。
▮▮▮▮⚝ size()
和 length()
:获取字符串长度。
▮▮▮▮⚝ capacity()
:获取字符串容量。
▮▮▮▮⚝ empty()
:判断字符串是否为空。
▮▮▮▮⚝ clear()
:清空字符串。
▮▮▮▮⚝ substr()
:获取子串。
▮▮▮▮⚝ compare()
:比较字符串。
▮▮▮▮⚝ operator+=
、operator+
、operator==
、operator!=
、operator<
、operator<=
、operator>
、operator>=
等操作符重载。
总的来说,fbstring
提供了与 std::string
几乎一致的接口,使得开发者可以很容易地从 std::string
切换到 fbstring
,并利用 fbstring
在性能和内存效率方面的优势。
2.1.3 fbstring 与 std::string 的互操作性 (Interoperability between fbstring and std::string)
在实际的 C++ 项目中,通常会同时使用 folly::fbstring
和 std::string
。为了方便混合使用这两种字符串类型,fbstring
提供了良好的互操作性。
① 隐式转换:
▮▮▮▮fbstring
可以隐式转换为 std::string_view
(C++17 引入) 和 folly::StringPiece
。这意味着在接受 std::string_view
或 folly::StringPiece
类型参数的函数中,可以直接传递 fbstring
对象。
▮▮▮▮但 std::string
不能隐式转换为 fbstring
,为了避免不必要的内存拷贝和类型混淆,需要显式转换。
② 显式转换:
▮▮▮▮可以通过 std::string
的构造函数从 fbstring
对象显式地构造 std::string
。
▮▮▮▮```cpp
▮▮▮▮#include
▮▮▮▮#include
▮▮▮▮#include
▮▮▮▮int main() {
▮▮▮▮ folly::fbstring fb_str = "hello folly";
▮▮▮▮ std::string std_str = std::string(fb_str); // 显式地从 fbstring 构造 std::string
▮▮▮▮ std::cout << "std_str: " << std_str << std::endl; // Output: std_str: hello folly
▮▮▮▮ return 0;
▮▮▮▮}
▮▮▮▮▮▮▮▮反之,也可以通过 `fbstring` 的构造函数从 `std::string` 对象显式地构造 `fbstring`。
▮▮▮▮
cpp
▮▮▮▮#include
▮▮▮▮#include
▮▮▮▮#include
▮▮▮▮int main() {
▮▮▮▮ std::string std_str = "hello std";
▮▮▮▮ folly::fbstring fb_str = folly::fbstring(std_str); // 显式地从 std::string 构造 fbstring
▮▮▮▮ std::cout << "fb_str: " << fb_str.c_str() << std::endl; // Output: fb_str: hello std
▮▮▮▮ return 0;
▮▮▮▮}
▮▮▮▮```
③ 使用 StringPiece
或 std::string_view
作为桥梁:
▮▮▮▮在函数接口设计中,如果函数只需要只读访问字符串,可以考虑使用 folly::StringPiece
或 std::string_view
作为参数类型。这样可以同时接受 fbstring
和 std::string
对象,避免不必要的类型转换和内存拷贝。
1
#include <folly/FBString.h>
2
#include <folly/StringPiece.h>
3
#include <string>
4
#include <iostream>
5
6
void print_string_piece(folly::StringPiece sp) {
7
std::cout << "StringPiece content: " << sp.toString() << std::endl; // 使用 toString() 转换为 std::string 输出
8
}
9
10
int main() {
11
folly::fbstring fb_str = "fbstring example";
12
std::string std_str = "std::string example";
13
14
print_string_piece(fb_str); // 隐式转换为 StringPiece
15
print_string_piece(std_str); // std::string 也可以隐式转换为 StringPiece
16
17
return 0;
18
}
通过上述互操作性机制,可以灵活地在 Folly 代码中混合使用 fbstring
和 std::string
,根据不同的场景和需求选择合适的字符串类型,同时保证代码的效率和可维护性。
2.2 folly::format:灵活且类型安全的格式化 (folly::format: Flexible and Type-Safe Formatting)
folly::format
是 Folly 库提供的一个强大且类型安全的字符串格式化工具,它旨在替代传统的 std::printf
和 std::stringstream
等格式化方法,提供更安全、更灵活、更易于扩展的字符串格式化方案。本节将介绍 folly::format
的用法、特性和优势,并通过对比 std::printf
和 std::stringstream
,展示其类型安全和扩展性。
2.2.1 format 语法与占位符 (format Syntax and Placeholders)
folly::format
使用类似于 Python 和 C# 的格式化字符串语法,通过占位符 {}
来表示需要格式化的参数。其基本语法结构如下:
1
folly::format("格式化字符串", 参数1, 参数2, ...);
① 基本占位符 {}
:
▮▮▮▮最简单的占位符是 {}
,它会按照参数的顺序依次替换占位符。
1
#include <folly/Format.h>
2
#include <iostream>
3
4
int main() {
5
int age = 30;
6
std::string name = "Alice";
7
8
folly::fbstring formatted_str = folly::format("My name is {}, and I am {} years old.", name, age);
9
std::cout << formatted_str.c_str() << std::endl; // Output: My name is Alice, and I am 30 years old.
10
11
return 0;
12
}
② 索引占位符 {index}
:
▮▮▮▮可以使用索引占位符 {index}
来指定使用哪个参数,索引从 0 开始。这允许参数被多次使用或以非顺序的方式使用。
1
#include <folly/Format.h>
2
#include <iostream>
3
4
int main() {
5
std::string item = "apple";
6
double price = 1.5;
7
8
folly::fbstring formatted_str = folly::format("The price of {0} is ${1:.2f}. {0} is delicious.", item, price);
9
std::cout << formatted_str.c_str() << std::endl; // Output: The price of apple is $1.50. apple is delicious.
10
return 0;
11
}
▮▮▮▮在上面的例子中,{0}
代表第一个参数 item
,{1}
代表第二个参数 price
,{1:.2f}
中的 :.2f
是格式控制选项,表示浮点数保留两位小数。
③ 格式控制选项:
▮▮▮▮folly::format
提供了丰富的格式控制选项,用于控制输出的格式,例如:
▮▮▮▮⚝ 宽度 (width):{:[width]}
,指定输出的最小宽度,不足宽度时会用空格填充。
▮▮▮▮⚝ 对齐 (alignment):{:[<>]width}
,>
右对齐,<
左对齐,^
居中对齐。
▮▮▮▮⚝ 填充字符 (fill character):{:[fill][alignment][width]}
,指定填充字符,默认为空格。
▮▮▮▮⚝ 精度 (precision):{:.precision}
,用于浮点数,指定小数点后的位数。
▮▮▮▮⚝ 类型 (type):{:[type]}
,指定输出类型,例如 d
(十进制整数), x
(十六进制整数), f
(浮点数), s
(字符串) 等。
1
#include <folly/Format.h>
2
#include <iostream>
3
4
int main() {
5
int num = 123;
6
double pi = 3.1415926;
7
std::string text = "Folly";
8
9
folly::fbstring formatted_str = folly::format(
10
"Number: {:5d}, Pi: {:8.2f}, Text: {:<10s}", num, pi, text);
11
std::cout << formatted_str.c_str() << std::endl; // Output: Number: 123, Pi: 3.14, Text: Folly
12
13
folly::fbstring formatted_str2 = folly::format(
14
"Number: {:05d}, Pi: {:*>8.3f}, Text: {:^10s}", num, pi, text);
15
std::cout << formatted_str2.c_str() << std::endl; // Output: Number: 00123, Pi: ***3.142, Text: Folly
16
17
return 0;
18
}
④ 自定义格式化标志 (flags):
▮▮▮▮folly::format
还支持自定义格式化标志,可以通过 folly::FormatFlag
枚举或自定义的标志类型来实现更精细的格式控制。
1
#include <folly/Format.h>
2
#include <iostream>
3
4
int main() {
5
int value = 255;
6
7
// 使用 # 标志输出带前缀的十六进制数
8
folly::fbstring formatted_str = folly::format("{:#x}", value);
9
std::cout << formatted_str.c_str() << std::endl; // Output: 0xff
10
11
// 使用 + 标志输出带符号的正数
12
int positive_num = 10;
13
folly::fbstring formatted_str2 = folly::format("{:+d}", positive_num);
14
std::cout << formatted_str2.c_str() << std::endl; // Output: +10
15
16
return 0;
17
}
folly::format
提供了丰富而灵活的语法和选项,可以满足各种复杂的字符串格式化需求。相比于 std::printf
和 std::stringstream
,folly::format
的语法更简洁、更易读,并且具有更好的类型安全性和扩展性。
2.2.2 类型安全与编译时检查 (Type Safety and Compile-Time Checks)
folly::format
最重要的特性之一是其类型安全性和编译时检查能力。与 std::printf
相比,folly::format
可以在编译时检查格式化字符串和参数类型是否匹配,从而避免运行时错误和安全漏洞。
① 类型安全:
▮▮▮▮std::printf
是 C 风格的格式化函数,它使用可变参数列表 (...)
,不会进行参数类型检查。如果格式化字符串中的占位符类型与实际参数类型不匹配,就会导致未定义的行为,甚至程序崩溃或安全漏洞。
▮▮▮▮例如,如果使用 %d
占位符来格式化一个浮点数,std::printf
不会报错,但输出结果可能是错误的,并且可能导致程序崩溃。
1
#include <cstdio>
2
#include <iostream>
3
4
int main() {
5
double pi = 3.1415926;
6
printf("Pi is %d\n", pi); // 错误的使用 %d 格式化浮点数,运行时不会报错,但结果错误
7
// std::cout << "Pi is " << pi << std::endl; // 正确的输出方式
8
9
return 0;
10
}
▮▮▮▮而 folly::format
是基于 C++ 模板实现的,它在编译时就能检查格式化字符串和参数类型是否匹配。如果类型不匹配,编译器会报错,从而在开发阶段就发现错误。
1
#include <folly/Format.h>
2
#include <iostream>
3
4
int main() {
5
double pi = 3.1415926;
6
// folly::format("Pi is {}\n", pi); // 正确的用法
7
// folly::format("Pi is %d\n", pi); // 编译错误:format string refers to argument of type double, but format specifier is for int
8
// 上面这行代码在编译时会报错,提示类型不匹配
9
10
std::cout << folly::format("Pi is {}\n", pi).c_str() << std::endl; // 正确的输出方式
11
12
return 0;
13
}
② 编译时检查:
▮▮▮▮folly::format
的编译时检查能力不仅限于类型匹配,还可以检查格式化字符串的语法错误,例如占位符格式错误、索引越界等。这些错误都可以在编译时被检测出来,大大提高了代码的健壮性。
1
#include <folly/Format.h>
2
#include <iostream>
3
4
int main() {
5
int num = 10;
6
// folly::format("Number: {0}, {1}", num); // 编译错误:format string refers to argument at index 1, but only 1 arguments were passed
7
// 上面这行代码在编译时会报错,提示索引 1 越界,因为只传递了一个参数
8
9
std::cout << folly::format("Number: {0}", num).c_str() << std::endl; // 正确的用法
10
11
return 0;
12
}
通过类型安全和编译时检查,folly::format
能够有效地避免因格式化字符串错误导致的运行时问题,提高代码的可靠性和安全性,尤其是在处理用户输入或外部数据时,类型安全显得尤为重要。
2.2.3 自定义格式化器与扩展 (Custom Formatters and Extensions)
folly::format
具有良好的扩展性,允许用户为自定义类型扩展格式化功能,实现灵活的输出控制。通过自定义格式化器 (formatter),可以控制自定义类型如何被 folly::format
格式化输出。
① 自定义格式化器的基本概念:
▮▮▮▮要为自定义类型 MyType
添加格式化支持,需要为 MyType
特化 folly::Formatter<MyType>
模板类,并实现 format
成员函数。format
函数负责将 MyType
对象格式化输出到 folly::FormatContext
上下文中。
② 示例:为自定义 Point 类型添加格式化支持:
▮▮▮▮假设有一个自定义的 Point
类型,表示二维平面上的点。
1
struct Point {
2
int x;
3
int y;
4
};
▮▮▮▮要让 folly::format
可以格式化输出 Point
对象,可以特化 folly::Formatter<Point>
:
1
#include <folly/Format.h>
2
#include <iostream>
3
4
struct Point {
5
int x;
6
int y;
7
};
8
9
namespace folly {
10
template <>
11
struct Formatter<Point> {
12
template <typename Context>
13
void format(Context& context, const Point& point) {
14
format_to(context, "({}, {})", point.x, point.y); // 使用 format_to 将 Point 的坐标格式化到 context
15
}
16
};
17
} // namespace folly
18
19
int main() {
20
Point p = {10, 20};
21
folly::fbstring formatted_str = folly::format("Point: {}", p);
22
std::cout << formatted_str.c_str() << std::endl; // Output: Point: (10, 20)
23
24
return 0;
25
}
▮▮▮▮在上面的例子中,folly::Formatter<Point>
的 format
函数使用 folly::format_to
辅助函数,将 Point
对象的 x
和 y
坐标格式化为 "(x, y)"
的字符串形式,并输出到 FormatContext
上下文中。
③ 处理格式控制选项:
▮▮▮▮自定义格式化器还可以处理格式控制选项。FormatContext
对象提供了访问格式标志、宽度、精度等信息的接口,可以根据这些信息来定制格式化输出。
1
#include <folly/Format.h>
2
#include <iostream>
3
4
struct Point {
5
int x;
6
int y;
7
};
8
9
namespace folly {
10
template <>
11
struct Formatter<Point> {
12
template <typename Context>
13
void format(Context& context, const Point& point) {
14
if (context.flags() & FormatFlag::kAlternateForm) { // 检查是否设置了 # 标志
15
format_to(context, "[x={}, y={}]", point.x, point.y); // 使用 [x=, y=] 格式
16
} else {
17
format_to(context, "({}, {})", point.x, point.y); // 使用 (,) 格式
18
}
19
}
20
};
21
} // namespace folly
22
23
int main() {
24
Point p = {10, 20};
25
folly::fbstring formatted_str1 = folly::format("Point: {}", p);
26
std::cout << formatted_str1.c_str() << std::endl; // Output: Point: (10, 20)
27
28
folly::fbstring formatted_str2 = folly::format("Point: {:#}", p); // 使用 # 标志
29
std::cout << formatted_str2.c_str() << std::endl; // Output: Point: [x=10, y=20]
30
31
return 0;
32
}
▮▮▮▮通过自定义格式化器,可以为任何自定义类型扩展 folly::format
的格式化功能,实现高度定制化的输出控制,使得 folly::format
成为一个非常灵活和强大的字符串格式化工具。
2.3 StringPiece:高效的字符串视图 (StringPiece: Efficient String View)
folly::StringPiece
是 Folly 库提供的轻量级字符串视图类,它表示一个字符串的非拥有的视图 (non-owning view)。StringPiece
本身并不拥有字符串数据,而是通过存储指向字符串数据的指针和长度来引用字符串。这种设计使得 StringPiece
可以在不拷贝字符串的情况下高效地操作字符串片段,尤其在字符串解析、查找等场景下非常有用。本节将介绍 folly::StringPiece
的概念、用途和优势,并讲解如何在不拷贝字符串的情况下高效地操作字符串片段。
2.3.1 StringPiece 的原理与应用场景 (Principles and Use Cases of StringPiece)
folly::StringPiece
的核心思想是零拷贝 (zero-copy) 字符串操作。它通过存储指向现有字符串数据的指针和长度,来创建一个字符串的视图,而不需要进行任何内存分配和字符串拷贝。
① StringPiece 的本质:
▮▮▮▮folly::StringPiece
本质上是一个结构体或类,内部通常包含两个成员变量:
▮▮▮▮⚝ 一个字符指针 (char*),指向字符串数据的起始位置。
▮▮▮▮⚝ 一个长度值 (size_t),表示字符串数据的长度。
▮▮▮▮StringPiece
对象本身并不拥有字符串数据的所有权,它只是对已存在的字符串数据的一个引用或视图。
② StringPiece 的创建:
▮▮▮▮可以从多种字符串类型创建 StringPiece
对象,例如 C 风格字符串、std::string
、folly::fbstring
等。创建 StringPiece
的过程通常是隐式的,或者通过构造函数显式创建,但都不会发生字符串拷贝。
1
#include <folly/StringPiece.h>
2
#include <string>
3
#include <folly/FBString.h>
4
#include <iostream>
5
6
int main() {
7
const char* c_str = "hello c string";
8
std::string std_str = "hello std string";
9
folly::fbstring fb_str = "hello fbstring";
10
11
folly::StringPiece sp1 = c_str; // 从 C 风格字符串创建 StringPiece
12
folly::StringPiece sp2 = std_str; // 从 std::string 创建 StringPiece
13
folly::StringPiece sp3 = fb_str; // 从 fbstring 创建 StringPiece
14
15
std::cout << "sp1: " << sp1.toString() << std::endl; // Output: sp1: hello c string
16
std::cout << "sp2: " << sp2.toString() << std::endl; // Output: sp2: hello std string
17
std::cout << "sp3: " << sp3.toString() << std::endl; // Output: sp3: hello fbstring
18
19
return 0;
20
}
▮▮▮▮在上面的例子中,从不同类型的字符串创建 StringPiece
对象,都没有发生实际的字符串数据拷贝。StringPiece
只是记录了指向原始字符串数据的指针和长度。
③ StringPiece 的应用场景:
▮▮▮▮StringPiece
在以下场景中特别有用:
▮▮▮▮⚝ 字符串解析 (Parsing):在解析字符串时,经常需要处理大量的子串操作。使用 StringPiece
可以避免在解析过程中频繁地进行字符串拷贝,提高解析效率。例如,在 HTTP 请求解析、日志解析等场景中,StringPiece
可以显著提升性能。
▮▮▮▮⚝ 字符串查找 (Searching):在字符串查找算法中,例如在大型文本中查找关键词,使用 StringPiece
可以避免不必要的字符串拷贝,提高查找效率。
▮▮▮▮⚝ 函数参数传递 (Function Argument Passing):当函数只需要只读访问字符串数据时,使用 StringPiece
作为参数类型,可以避免函数调用时的字符串拷贝开销,尤其是在处理大型字符串时,性能提升非常明显。
▮▮▮▮⚝ 避免不必要的拷贝:在很多情况下,我们只需要读取字符串的内容,而不需要修改它。这时使用 StringPiece
可以明确表示只读意图,并避免潜在的拷贝操作。
1
#include <folly/StringPiece.h>
2
#include <iostream>
3
4
void process_substring(folly::StringPiece substring) {
5
std::cout << "Processing substring: " << substring.toString() << std::endl;
6
}
7
8
int main() {
9
std::string long_string = "This is a very long string for demonstration.";
10
folly::StringPiece whole_string = long_string;
11
12
// 创建子串视图,不发生拷贝
13
folly::StringPiece sub_piece = whole_string.substr(10, 10); // 从索引 10 开始,长度为 10 的子串
14
15
process_substring(sub_piece); // 将子串视图传递给函数,不发生拷贝
16
17
return 0;
18
}
▮▮▮▮在上面的例子中,substr
操作和函数参数传递都使用了 StringPiece
,避免了字符串拷贝,提高了效率。
2.3.2 StringPiece 的常用操作与注意事项 (Common Operations and Considerations of StringPiece)
folly::StringPiece
提供了丰富的成员函数,用于操作字符串视图,例如判空、取子串、比较等。但由于 StringPiece
本身不拥有字符串数据,因此在使用时需要注意其生命周期管理。
① 常用操作:
▮▮▮▮StringPiece
提供了许多与字符串操作相关的成员函数,但都是只读操作,不会修改字符串内容,也不会发生内存分配和拷贝。常用的操作包括:
▮▮▮▮⚝ empty()
:判断 StringPiece
是否为空。
▮▮▮▮⚝ size()
和 length()
:获取 StringPiece
的长度。
▮▮▮▮⚝ data()
:获取指向字符串数据的指针。
▮▮▮▮⚝ begin()
和 end()
:获取迭代器,用于遍历字符串。
▮▮▮▮⚝ operator[]
和 at()
:访问指定位置的字符。
▮▮▮▮⚝ substr()
:获取子串视图。
▮▮▮▮⚝ remove_prefix()
和 remove_suffix()
:移除前缀或后缀,得到新的 StringPiece
视图。
▮▮▮▮⚝ startswith()
和 endswith()
:判断是否以指定前缀或后缀开始或结束。
▮▮▮▮⚝ compare()
:比较两个 StringPiece
对象。
▮▮▮▮⚝ toStdString()
和 toString()
:转换为 std::string
对象(会发生拷贝)。
1
#include <folly/StringPiece.h>
2
#include <iostream>
3
4
int main() {
5
folly::StringPiece sp = "string piece example";
6
7
std::cout << "Size: " << sp.size() << std::endl; // Output: Size: 19
8
std::cout << "Is empty: " << sp.empty() << std::endl; // Output: Is empty: 0
9
std::cout << "First char: " << sp[0] << std::endl; // Output: First char: s
10
11
folly::StringPiece sub_sp = sp.substr(7, 5); // 获取子串 "piece" 的视图
12
std::cout << "Substring: " << sub_sp.toString() << std::endl; // Output: Substring: piece
13
14
folly::StringPiece prefix_removed_sp = sp.remove_prefix(7); // 移除前 7 个字符
15
std::cout << "Prefix removed: " << prefix_removed_sp.toString() << std::endl; // Output: Prefix removed: piece example
16
17
bool starts_with_str = sp.startswith("string");
18
std::cout << "Starts with 'string': " << starts_with_str << std::endl; // Output: Starts with 'string': 1
19
20
return 0;
21
}
② 生命周期管理的重要性:
▮▮▮▮由于 StringPiece
不拥有字符串数据的所有权,因此在使用 StringPiece
时,必须确保其引用的原始字符串数据在 StringPiece
对象的使用期间仍然有效。如果原始字符串数据被释放或销毁,StringPiece
对象就会变成悬空指针 (dangling pointer),访问它会导致未定义的行为。
▮▮▮▮例如,如果 StringPiece
引用的是一个局部变量的字符串,当局部变量的作用域结束时,字符串数据会被销毁,此时 StringPiece
就失效了。
1
#include <folly/StringPiece.h>
2
#include <iostream>
3
4
folly::StringPiece get_string_piece() {
5
std::string local_string = "local string";
6
folly::StringPiece sp = local_string; // sp 引用 local_string
7
return sp; // 返回 StringPiece,但 local_string 在函数结束后会被销毁
8
}
9
10
int main() {
11
folly::StringPiece dangling_sp = get_string_piece(); // dangling_sp 变成悬空指针
12
// std::cout << dangling_sp.toString() << std::endl; // 错误:访问悬空指针,未定义行为
13
14
return 0;
15
}
▮▮▮▮为了避免生命周期问题,通常需要确保 StringPiece
引用的字符串数据具有更长的生命周期,例如全局变量、静态变量、堆上分配的内存,或者在 StringPiece
的使用范围内,原始字符串数据仍然有效。
▮▮▮▮在函数参数传递时,如果函数只接受 StringPiece
参数,调用者需要负责管理原始字符串数据的生命周期,确保在函数执行期间数据有效。
总而言之,folly::StringPiece
是一种高效、零拷贝的字符串视图类,适用于需要频繁操作字符串片段,但又不想付出拷贝代价的场景。但使用 StringPiece
时,务必注意其生命周期管理,确保引用的原始字符串数据在 StringPiece
的使用期间保持有效。
3. 容器与数据结构:高效的数据组织与管理 (Containers and Data Structures: Efficient Data Organization and Management)
本章深入剖析 Folly 库提供的各种高效容器和数据结构,包括 FBVector
、F14Map
等,帮助读者选择合适的工具来优化数据存储和访问性能。
3.1 FBVector:优化的动态数组 (FBVector: Optimized Dynamic Array)
详细介绍 FBVector
的特性、内存分配策略和性能优势,对比 std::vector
,分析其适用场景。
3.1.1 FBVector 的内存增长策略与预分配 (Memory Growth Strategy and Pre-allocation of FBVector)
FBVector
是 Folly 库提供的动态数组实现,它在 std::vector
的基础上进行了优化,尤其是在内存管理方面。理解 FBVector
的内存增长策略和预分配机制,可以帮助我们更好地利用它来提升性能,并避免不必要的内存重新分配开销。
① 内存增长策略 (Memory Growth Strategy):
与 std::vector
类似,FBVector
在容量不足时也会自动增长。默认情况下,std::vector
的增长策略是当容量不足时,通常会分配一块新的、更大的内存区域,并将原有数据复制到新的内存区域,然后释放旧的内存区域。常见的增长因子是 1.5 或 2,这意味着每次容量翻倍或增长 50%。
FBVector
也采用了类似的增长策略,但具体实现上可能存在细微差别,Folly 可能会根据实际的性能测试和应用场景进行调整,以达到更优的性能。 具体的增长因子和策略可能在不同 Folly 版本中有所调整,但其核心思想仍然是在时间和空间之间找到平衡点,既要避免频繁的内存重新分配,又要避免过度浪费内存。
② 预分配 (Pre-allocation):
预分配是指在向 FBVector
中添加元素之前,预先分配一定量的内存空间。这样做的好处是可以减少甚至避免在后续元素插入过程中因容量不足而导致的内存重新分配和数据复制的开销,尤其是在我们预先知道或可以估计 FBVector
大小时,预分配可以显著提升性能。
FBVector
提供了 reserve(size_type n)
方法用于预分配至少能容纳 n
个元素的内存空间。调用 reserve()
并不会改变 FBVector
的 size()
,而只会影响 capacity()
。size()
表示当前 FBVector
中实际存储的元素个数,而 capacity()
表示 FBVector
当前已分配的内存空间可以容纳的元素个数。
示例代码:
1
#include <folly/container/FBVector.h>
2
#include <iostream>
3
4
int main() {
5
folly::FBVector<int> vec;
6
7
std::cout << "Initial size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl; // 初始大小和容量
8
9
vec.reserve(100); // 预分配 100 个元素的空间
10
std::cout << "After reserve(100), size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl; // 预分配后的容量
11
12
for (int i = 0; i < 100; ++i) {
13
vec.push_back(i); // 添加 100 个元素,无需重新分配内存
14
}
15
std::cout << "After push_back 100 elements, size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl; // 添加元素后的容量和大小
16
17
vec.push_back(100); // 超过预分配容量,可能触发重新分配
18
std::cout << "After push_back 101 elements, size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl; // 再次添加元素后的容量和大小
19
20
return 0;
21
}
代码解释:
⚝ 首先,我们创建一个空的 folly::FBVector<int>
实例 vec
。
⚝ 使用 reserve(100)
预分配了 100 个 int
元素的空间。此时,size()
仍然是 0,但 capacity()
至少为 100。
⚝ 循环添加 100 个元素,由于之前已经预分配了足够的空间,因此在添加这 100 个元素的过程中,通常不会发生内存重新分配。
⚝ 当尝试添加第 101 个元素时,由于超出了预分配的容量,FBVector
可能会触发内存重新分配,容量会增长到更大的值,以容纳新的元素。
性能考量:
⚝ 何时使用预分配: 当你大致知道或可以估计 FBVector
需要存储的元素数量时,强烈建议使用 reserve()
进行预分配。这在循环添加大量元素、从其他容器复制元素等场景中尤其有效。
⚝ 避免过度预分配: 预分配虽然可以提升性能,但不宜过度。过度预分配会浪费内存,尤其是在元素数量远小于预分配容量时。合理的预分配容量应该是根据实际需求和内存资源进行权衡的结果。
⚝ 与 std::vector
的对比: std::vector
也提供 reserve()
方法进行预分配,FBVector
在预分配机制上与 std::vector
基本一致。主要的性能差异可能体现在更底层的内存分配器和更细致的增长策略调整上,这些优化旨在提升 FBVector
在特定场景下的性能表现。
总而言之,理解 FBVector
的内存增长策略和预分配机制,可以帮助开发者编写出更高效的 C++ 代码,尤其是在处理大量数据或对性能有较高要求的场景中。合理利用预分配可以显著减少内存操作的开销,提升程序的整体性能。
3.1.2 FBVector 的 emplace_back 与移动语义 (emplace_back and Move Semantics in FBVector)
emplace_back()
和 移动语义 (move semantics) 是 C++11 引入的重要特性,它们在提高容器操作效率方面起着关键作用。FBVector
充分利用了这些特性,提供了 emplace_back()
方法,并受益于移动语义,从而在对象插入时实现更高的效率。
① emplace_back() 的优势 (Advantages of emplace_back()):
emplace_back()
是 C++11 容器引入的一个成员函数,与 push_back()
类似,用于在容器尾部添加新元素。但 emplace_back()
的核心优势在于它是直接在容器的内存空间中构造元素,而不是像 push_back()
那样,先构造临时对象,再将临时对象复制或移动到容器中。
具体优势:
⚝ 避免不必要的拷贝或移动操作: 对于非平凡构造的对象 (例如,构造函数需要进行复杂操作的对象,或者具有复杂的资源管理的对象),emplace_back()
可以直接在容器内部构造对象,省去了先外部构造再拷贝或移动的步骤,从而减少了构造和析构的开销。
⚝ 原地构造,效率更高: emplace_back()
接受可变参数 (variadic arguments),这些参数会被直接传递给元素的构造函数,用于在容器内部原地构造元素。这种方式更加直接高效。
⚝ 完美转发 (perfect forwarding): emplace_back()
内部使用了完美转发,可以将传入的参数完美地转发给元素的构造函数,保持参数的值类别 (value category) (左值或右值)。
示例代码对比 push_back()
和 emplace_back()
:
1
#include <folly/container/FBVector.h>
2
#include <iostream>
3
4
class MyObject {
5
public:
6
MyObject(int id) : id_(id) {
7
std::cout << "MyObject constructed with id: " << id_ << std::endl;
8
}
9
MyObject(const MyObject& other) : id_(other.id_) {
10
std::cout << "MyObject copy constructed from id: " << id_ << std::endl;
11
}
12
MyObject(MyObject&& other) noexcept : id_(other.id_) {
13
std::cout << "MyObject move constructed from id: " << id_ << std::endl;
14
}
15
~MyObject() {
16
std::cout << "MyObject destructed with id: " << id_ << std::endl;
17
}
18
private:
19
int id_;
20
};
21
22
int main() {
23
folly::FBVector<MyObject> vec1;
24
std::cout << "Using push_back:" << std::endl;
25
vec1.push_back(MyObject(1)); // 使用 push_back
26
27
folly::FBVector<MyObject> vec2;
28
std::cout << "\nUsing emplace_back:" << std::endl;
29
vec2.emplace_back(2); // 使用 emplace_back
30
31
return 0;
32
}
代码输出 (可能因编译器优化而略有不同):
1
Using push_back:
2
MyObject constructed with id: 1
3
MyObject move constructed from id: 1 // 或 copy constructed,取决于编译器优化
4
MyObject destructed with id: 1
5
Using emplace_back:
6
MyObject constructed with id: 2
7
MyObject destructed with id: 2
代码解释:
⚝ 在使用 push_back()
时,首先在外部 MyObject(1)
构造了一个临时对象,然后该临时对象被移动构造 (或拷贝构造,取决于编译器优化) 到 vec1
中,最后临时对象被析构。因此,push_back()
涉及一次构造、一次移动 (或拷贝) 构造和一次析构。
⚝ 在使用 emplace_back()
时,vec2.emplace_back(2)
直接在 vec2
内部使用参数 2
构造 MyObject
对象,只涉及一次构造和一次析构 (在 vec2
析构时)。因此,emplace_back()
减少了一次移动 (或拷贝) 构造的开销。
② 移动语义的应用 (Application of Move Semantics):
移动语义 (move semantics) 也是 C++11 引入的重要特性,旨在避免不必要的对象拷贝操作,尤其是在处理资源密集型的对象时 (例如,管理大量内存的对象)。移动语义允许我们将资源的所有权从一个对象转移到另一个对象,而不是进行深拷贝。
FBVector
在内部操作 (例如,扩容时的元素转移) 和与外部交互时 (例如,push_back()
接受右值引用) 都广泛使用了移动语义。当插入的对象是右值 (rvalue) 或可以使用 std::move()
转换为右值时,FBVector
会优先使用移动构造,而不是拷贝构造,从而提高效率。
示例代码演示移动语义的优势:
1
#include <folly/container/FBVector.h>
2
#include <iostream>
3
#include <utility> // std::move
4
5
class ResourceHolder {
6
public:
7
ResourceHolder() : data_(new int[1000000]) {
8
std::cout << "ResourceHolder constructed, memory allocated." << std::endl;
9
}
10
~ResourceHolder() {
11
delete[] data_;
12
std::cout << "ResourceHolder destructed, memory freed." << std::endl;
13
}
14
ResourceHolder(const ResourceHolder& other) : data_(new int[1000000]) { // 拷贝构造函数
15
std::cout << "ResourceHolder copy constructed, deep copy." << std::endl;
16
std::copy(other.data_, other.data_ + 1000000, data_); // 深拷贝
17
}
18
ResourceHolder(ResourceHolder&& other) noexcept : data_(other.data_) { // 移动构造函数
19
std::cout << "ResourceHolder move constructed, ownership transferred." << std::endl;
20
other.data_ = nullptr; // 转移所有权,避免 double free
21
}
22
private:
23
int* data_;
24
};
25
26
int main() {
27
folly::FBVector<ResourceHolder> vec;
28
29
std::cout << "Pushing back a temporary ResourceHolder (move semantics used):" << std::endl;
30
vec.push_back(ResourceHolder()); // 插入临时对象,触发移动构造
31
32
ResourceHolder holder;
33
std::cout << "\nPushing back an existing ResourceHolder (copy semantics used if no move, or explicit move):" << std::endl;
34
// vec.push_back(holder); // 如果没有移动语义,会触发拷贝构造 (开销大)
35
vec.push_back(std::move(holder)); // 显式使用 std::move,触发移动构造 (高效)
36
37
return 0;
38
}
代码输出 (可能因编译器优化而略有不同):
1
Pushing back a temporary ResourceHolder (move semantics used):
2
ResourceHolder constructed, memory allocated.
3
ResourceHolder move constructed, ownership transferred.
4
ResourceHolder destructed, memory freed.
5
Pushing back an existing ResourceHolder (copy semantics used if no move, or explicit move):
6
ResourceHolder move constructed, ownership transferred.
7
ResourceHolder destructed, memory freed.
8
ResourceHolder destructed, memory freed.
代码解释:
⚝ ResourceHolder
类管理一块较大的内存资源。拷贝构造函数执行深拷贝,开销较大;移动构造函数只转移资源所有权,开销很小。
⚝ 当使用 vec.push_back(ResourceHolder())
插入临时对象时,由于 ResourceHolder()
返回的是右值,FBVector
的 push_back()
会自动调用移动构造函数,避免深拷贝。
⚝ 当插入已存在的对象 holder
时,如果直接使用 vec.push_back(holder)
,会调用拷贝构造函数 (除非编译器进行了 RVO/NRVO 优化)。为了显式触发移动语义,可以使用 std::move(holder)
将 holder
转换为右值引用,强制 push_back()
调用移动构造函数。
总结:
⚝ emplace_back()
通过原地构造元素,避免了不必要的拷贝或移动操作,提高了插入效率,尤其适用于非平凡构造的对象。
⚝ 移动语义允许高效地转移资源所有权,FBVector
充分利用移动语义,在内部操作和接口设计中都尽可能地减少拷贝开销。
⚝ 在向 FBVector
插入对象时,优先考虑使用 emplace_back()
,并尽可能利用移动语义 (例如,插入临时对象或使用 std::move()
显式移动)。
⚝ 理解 emplace_back()
和移动语义,可以帮助开发者编写出更高效、更现代的 C++ 代码,充分发挥 C++11 及以后标准的新特性优势。
3.2 F14Map 与 F14Set:高性能哈希容器 (F14Map and F14Set: High-Performance Hash Containers)
介绍 Folly 库的高性能哈希容器 F14Map
和 F14Set
,分析其哈希算法、冲突解决策略和性能特点。
3.2.1 F14 哈希算法与性能分析 (F14 Hash Algorithm and Performance Analysis)
F14Map
和 F14Set
是 Folly 库中高性能的哈希容器实现,它们的设计目标是在各种常见场景下,提供优于 std::unordered_map
和 std::unordered_set
的性能。F14
系列哈希容器的核心竞争力之一,就是其高效的 F14 哈希算法以及优化的冲突解决策略。
① F14 哈希算法的设计原理 (Design Principles of F14 Hash Algorithm):
F14 哈希算法并非单一的算法,而是一系列针对不同数据类型和应用场景优化的哈希算法的集合。其设计目标主要集中在以下几个方面:
⚝ 高速性 (Speed): 哈希算法的首要目标是计算速度快,尤其是在需要频繁进行哈希计算的哈希容器中。F14 哈希算法力求在保证良好分布性的前提下,尽可能减少计算开销。
⚝ 良好的分布性 (Good Distribution): 良好的哈希算法应该能够将键值均匀地分布到哈希表的各个桶 (bucket) 中,避免哈希冲突过于集中,从而影响哈希容器的性能。F14 哈希算法针对常见的数据类型 (如整数、字符串等) 进行了专门优化,以提高分布性。
⚝ 抗碰撞攻击 (Collision Resistance) (在某些变体中): 对于一些安全敏感的应用场景,哈希算法还需要具备一定的抗碰撞攻击能力,即难以人为构造出大量哈希冲突的输入数据。Folly 提供了 HashingAlgorithm<>::kCityHashCrc128
等加密哈希算法选项,以满足这类需求。
⚝ 针对不同数据类型的优化 (Optimizations for Different Data Types): F14 哈希算法针对常见的数据类型 (如整数、字符串、指针等) 提供了专门的优化版本。例如,对于整数类型,可能采用简单的乘法哈希或位运算哈希;对于字符串类型,可能采用多项式哈希或基于 SIMD 指令的并行哈希。
F14 哈希算法的常见变体:
Folly 的 HashingAlgorithm
模板类提供了多种哈希算法选项,常见的包括:
⚝ HashingAlgorithm<>::kFastModuloMultiply
: 一种快速的乘法哈希算法,适用于整数类型,速度快,但分布性可能略逊于更复杂的算法。
⚝ HashingAlgorithm<>::kCityHash64
: 来自 Google CityHash 库的 64 位哈希算法,速度和分布性都很好,是 F14 哈希容器的默认哈希算法。
⚝ HashingAlgorithm<>::kMurmurHash2
和 HashingAlgorithm<>::kMurmurHash3
: 来自 MurmurHash 库的哈希算法,也是速度和分布性俱佳的通用哈希算法。
⚝ HashingAlgorithm<>::kFarmHash64
: 来自 Google FarmHash 库的 64 位哈希算法,在处理长字符串时可能比 CityHash 更快。
⚝ HashingAlgorithm<>::kXXHash64
: 来自 XXHash 库的 64 位哈希算法,以极速著称,但分布性可能略逊于 CityHash 和 MurmurHash。
⚝ HashingAlgorithm<>::kMeowHash64
: 一种较新的高速哈希算法,性能接近 XXHash,但可能提供更好的分布性。
⚝ HashingAlgorithm<>::kMetroHash64
: 另一种高速哈希算法,也常用于高性能哈希容器。
⚝ HashingAlgorithm<>::kSpookyHashV2
: 来自 SpookyHash 库的哈希算法,特点是实现简单,速度较快。
⚝ HashingAlgorithm<>::kHighwayHash64
: 来自 Google HighwayHash 库的哈希算法,针对现代 CPU 架构优化,速度极快,尤其是在支持 AVX2/AVX-512 指令集的平台上。
⚝ HashingAlgorithm<>::kT1ha0_64
和 HashingAlgorithm<>::kT1ha1_64
: 来自 T1ha 库的哈希算法,速度快,分布性好,且实现相对简单。
⚝ HashingAlgorithm<>::kSipHash24
和 HashingAlgorithm<>::kSipHash48
: SipHash 算法,主要用于抵抗哈希碰撞攻击,安全性较高,但速度相对较慢。
⚝ HashingAlgorithm<>::kCityHashCrc128
: CityHash 库的 128 位哈希算法,加密哈希算法,安全性高,但速度较慢。
② 性能分析与对比 (Performance Analysis and Comparison):
F14 哈希算法的性能优势主要体现在速度和分布性两个方面。通过精选和优化哈希算法,并结合优化的冲突解决策略,F14Map
和 F14Set
在许多场景下都能超越 std::unordered_map
和 std::unordered_set
的性能。
性能优势来源:
⚝ 快速哈希算法: F14 系列哈希容器默认使用 CityHash64
等高速哈希算法,这些算法经过精心设计和优化,在保证良好分布性的前提下,尽可能减少计算开销。相比之下,std::unordered_map
和 std::unordered_set
的默认哈希算法可能在某些场景下性能稍逊。
⚝ 优化的冲突解决策略: F14Map
和 F14Set
通常采用开放寻址法 (open addressing) 或 链地址法 (separate chaining) 的变体来解决哈希冲突。Folly 可能会对这些策略进行定制化优化,例如使用更高效的探测序列 (probe sequence) 或更紧凑的链表结构,以减少冲突带来的性能损失。
⚝ 针对特定数据类型的优化: F14 哈希算法针对常见的数据类型进行了专门优化,例如使用更高效的哈希函数或特殊的处理逻辑。这使得 F14 哈希容器在处理特定类型数据时,能够获得更好的性能。
⚝ 内存局部性优化: Folly 可能会在 F14Map
和 F14Set
的实现中,更加注重内存局部性,例如使用连续的内存块存储哈希表数据,以提高缓存命中率,从而提升性能。
性能对比 (与 std::unordered_map
和 std::unordered_set
):
⚝ 插入 (Insertion): 在大多数情况下,F14Map
和 F14Set
的插入性能优于或持平于 std::unordered_map
和 std::unordered_set
。在高负载因子 (high load factor) 或哈希冲突较多的情况下,F14
系列容器的优势可能更加明显。
⚝ 查找 (Lookup): F14Map
和 F14Set
的查找性能通常也优于或持平于 std::unordered_map
和 std::unordered_set
。在键值分布均匀的情况下,两者性能接近;在键值分布不均匀或存在大量哈希冲突的情况下,F14
系列容器的优势可能更加明显。
⚝ 删除 (Deletion): F14Map
和 F14Set
的删除性能与 std::unordered_map
和 std::unordered_set
接近,但具体表现可能取决于冲突解决策略和内存管理实现的细节。
⚝ 迭代 (Iteration): 由于 std::unordered_map
和 std::unordered_set
通常采用链地址法,其迭代性能可能略优于采用开放寻址法的 F14Map
和 F14Set
。但实际差距可能不大,且迭代性能通常不是哈希容器的主要性能瓶颈。
⚝ 内存占用 (Memory Footprint): F14Map
和 F14Set
的内存占用可能略高于 std::unordered_map
和 std::unordered_set
,这取决于具体的实现细节和负载因子。但 Folly 通常会努力在性能和内存占用之间找到平衡点。
选择哈希算法 (Choosing a Hash Algorithm):
F14Map
和 F14Set
允许用户自定义哈希算法。可以通过模板参数来指定要使用的哈希算法。例如:
1
#include <folly/container/F14Map.h>
2
#include <folly/hash/HashingAlgorithm.h>
3
4
// 使用 F14Map,默认哈希算法 (通常是 CityHash64)
5
folly::F14Map<int, std::string> map1;
6
7
// 使用 F14Map,指定 MurmurHash3 哈希算法
8
using MyMapType = folly::F14Map<int, std::string, folly::HashingAlgorithm<folly::HashingAlgorithmKind::kMurmurHash3>>;
9
MyMapType map2;
10
11
// 使用 F14Map,指定 XXHash64 哈希算法
12
using MyMapType2 = folly::F14Map<int, std::string, folly::HashingAlgorithm<folly::HashingAlgorithmKind::kXXHash64>>;
13
MyMapType2 map3;
何时选择 F14 哈希容器:
⚝ 对性能有较高要求: 如果你的应用场景对哈希容器的插入、查找性能有较高要求,并且你希望尽可能地榨取性能潜力,那么 F14Map
和 F14Set
是一个值得考虑的选择。
⚝ 需要处理大量数据: 在处理大规模数据集时,哈希容器的性能差异可能会被放大。F14
系列容器的高性能优势可能更加明显。
⚝ 兼容 Folly 库: 如果你的项目已经使用了 Folly 库,或者你计划引入 Folly 库,那么使用 F14Map
和 F14Set
可以更好地融入 Folly 生态系统,并与其他 Folly 组件协同工作。
⚝ 需要更丰富的哈希算法选项: Folly 提供了更丰富的哈希算法选项,可以根据具体的应用场景和数据类型选择最合适的哈希算法。
注意事项:
⚝ 性能测试: 虽然 F14Map
和 F14Set
在许多场景下都表现出色,但最佳的性能表现仍然取决于具体的应用场景、数据类型、负载因子、哈希冲突情况等因素。建议在实际应用中进行性能测试,以验证 F14 系列容器是否真的能带来性能提升。
⚝ API 兼容性: F14Map
和 F14Set
的 API 与 std::unordered_map
和 std::unordered_set
基本兼容,因此替换成本较低。但仍然需要注意一些细微的差异,例如自定义哈希函数和相等比较函数的接口。
总而言之,F14Map
和 F14Set
是 Folly 库提供的高性能哈希容器,它们通过精选的 F14 哈希算法和优化的冲突解决策略,在许多场景下都能提供优于 std::unordered_map
和 std::unordered_set
的性能。对于对性能有较高要求的 C++ 应用,F14
系列容器是一个值得深入了解和尝试的选项。
3.2.2 F14Map 与 F14Set 的 API 与用法 (APIs and Usage of F14Map and F14Set)
F14Map
和 F14Set
的 API 设计很大程度上参考了 std::unordered_map
和 std::unordered_set
,因此熟悉 STL 哈希容器的开发者可以快速上手 F14
系列容器。本节将介绍 F14Map
和 F14Set
的常用 API 和用法,并强调一些使用注意事项。
① F14Map 的常用 API 与用法 (Common APIs and Usage of F14Map):
F14Map<Key, T, Hash, Equal, Allocator>
是一个关联容器,存储键值对 (key-value pairs),其中每个键是唯一的。F14Map
提供了以下常用 API:
⚝ 构造函数 (Constructors):
▮▮▮▮⚝ F14Map()
: 默认构造函数,创建一个空的 F14Map
。
▮▮▮▮⚝ F14Map(size_type n)
: 构造函数,预留至少能容纳 n
个元素的空间,类似于 std::unordered_map
的 reserve()
,但 F14Map
的构造函数可以直接指定初始容量。
▮▮▮▮⚝ F14Map(InputIterator first, InputIterator last)
: 范围构造函数,使用迭代器范围 [first, last)
内的元素初始化 F14Map
。
▮▮▮▮⚝ F14Map(std::initializer_list<value_type> il)
: 初始化列表构造函数,使用初始化列表 il
初始化 F14Map
。
▮▮▮▮⚝ 拷贝构造函数、移动构造函数等。
⚝ 插入 (Insertion):
▮▮▮▮⚝ std::pair<iterator, bool> insert(const value_type& value)
▮▮▮▮⚝ std::pair<iterator, bool> insert(value_type&& value)
▮▮▮▮⚝ template <class... Args> std::pair<iterator, bool> emplace(Args&&... args)
▮▮▮▮⚝ iterator insert(const_iterator hint, const value_type& value)
▮▮▮▮⚝ iterator insert(const_iterator hint, value_type&& value)
▮▮▮▮⚝ template <class... Args> iterator emplace_hint(const_iterator hint, Args&&... args)
▮▮▮▮⚝ template <class InputIterator> void insert(InputIterator first, InputIterator last)
▮▮▮▮⚝ void insert(std::initializer_list<value_type> il)
与 std::unordered_map
类似,F14Map
的 insert()
和 emplace()
方法返回一个 std::pair
,其中 first
是指向插入位置的迭代器,second
是一个 bool
值,表示是否成功插入 (如果键已存在,则插入失败,second
为 false
)。emplace()
具有 原地构造 的优势,效率更高。
⚝ 查找 (Lookup):
▮▮▮▮⚝ iterator find(const Key& key)
▮▮▮▮⚝ const_iterator find(const Key& key) const
▮▮▮▮⚝ size_type count(const Key& key) const
: 返回键为 key
的元素个数 (由于键唯一,返回值只能是 0 或 1)。
▮▮▮▮⚝ iterator at(const Key& key)
▮▮▮▮⚝ const_iterator at(const Key& key) const
▮▮▮▮⚝ T& operator[](const Key& key)
▮▮▮▮⚝ T& operator[](Key&& key)
: 注意,operator[]
如果键不存在,会插入一个新的键值对,并返回新插入值的引用。
▮▮▮▮⚝ T* get_ptr(const Key& key)
: Folly 提供的扩展方法,返回指向值的裸指针 (raw pointer),如果键不存在,则返回 nullptr
。
▮▮▮▮⚝ const T* get_ptr(const Key& key) const
: const
版本的 get_ptr()
。
find()
方法返回指向找到元素的迭代器,如果未找到,则返回 end()
迭代器。at()
方法返回值的引用,如果键不存在,则抛出异常 std::out_of_range
。operator[]
提供了类似数组的访问方式,但需要注意其插入行为。get_ptr()
是一种更安全的查找方式,避免了异常和默认插入。
⚝ 删除 (Deletion):
▮▮▮▮⚝ size_type erase(const Key& key)
: 删除键为 key
的元素,返回删除的元素个数 (0 或 1)。
▮▮▮▮⚝ iterator erase(const_iterator pos)
: 删除迭代器 pos
指向的元素,返回指向被删除元素之后元素的迭代器。
▮▮▮▮⚝ iterator erase(const_iterator first, const_iterator last)
: 删除迭代器范围 [first, last)
内的元素,返回指向最后一个被删除元素之后元素的迭代器。
▮▮▮▮⚝ void clear()
: 清空 F14Map
中的所有元素。
⚝ 容量 (Capacity):
▮▮▮▮⚝ bool empty() const
: 判断 F14Map
是否为空。
▮▮▮▮⚝ size_type size() const
: 返回 F14Map
中元素的个数。
▮▮▮▮⚝ size_type max_size() const
: 返回 F14Map
可以容纳的最大元素个数 (受系统内存限制)。
▮▮▮▮⚝ size_type bucket_count() const
: 返回哈希表的桶 (bucket) 的数量。
▮▮▮▮⚝ float load_factor() const
: 返回负载因子 (平均每个桶的元素数量)。
▮▮▮▮⚝ float max_load_factor() const
: 获取或设置最大负载因子,当负载因子超过最大负载因子时,哈希表会进行 rehash (重新哈希) 操作,增加桶的数量,以降低负载因子,提升性能。
▮▮▮▮⚝ void rehash(size_type n)
: 强制进行 rehash 操作,将桶的数量增加到至少 n
。
▮▮▮▮⚝ void reserve(size_type n)
: 预留至少能容纳 n
个元素的空间,但不会改变桶的数量,只是预先分配内存,减少后续插入时的 rehash 开销。
⚝ 迭代器 (Iterators):
▮▮▮▮⚝ iterator begin()
/ const_iterator begin() const
▮▮▮▮⚝ iterator end()
/ const_iterator end() const
▮▮▮▮⚝ reverse_iterator rbegin()
/ const_reverse_iterator rbegin() const
▮▮▮▮⚝ reverse_iterator rend()
/ const_reverse_iterator rend() const
▮▮▮▮⚝ cbegin()
/ cend()
/ crbegin()
/ crend()
(C++11 引入)
F14Map
的迭代器遍历元素时,不保证元素顺序,因为哈希容器是无序的。
⚝ 其他操作 (Other Operations):
▮▮▮▮⚝ void swap(F14Map& other)
: 交换两个 F14Map
对象的内容。
▮▮▮▮⚝ bool operator==(const F14Map& other) const
/ operator!=(const F14Map& other) const
: 比较两个 F14Map
对象是否相等 (元素和顺序都相同)。
▮▮▮▮⚝ template <class K, class V, class H, class E, class A> void swap(F14Map<K, V, H, E, A>& a, F14Map<K, V, H, E, A>& b)
: 非成员函数 swap()
。
② F14Set 的常用 API 与用法 (Common APIs and Usage of F14Set):
F14Set<Key, Hash, Equal, Allocator>
是一个集合容器,存储唯一的键值 (keys)。F14Set
的 API 与 F14Map
非常相似,主要区别在于 F14Set
只存储键,不存储值。
⚝ 构造函数 (Constructors): 与 F14Map
类似。
⚝ 插入 (Insertion):
▮▮▮▮⚝ std::pair<iterator, bool> insert(const Key& value)
▮▮▮▮⚝ std::pair<iterator, bool> insert(Key&& value)
▮▮▮▮⚝ template <class... Args> std::pair<iterator, bool> emplace(Args&&... args)
▮▮▮▮⚝ iterator insert(const_iterator hint, const Key& value)
▮▮▮▮⚝ iterator insert(const_iterator hint, Key&& value)
▮▮▮▮⚝ template <class... Args> iterator emplace_hint(const_iterator hint, Args&&... args)
▮▮▮▮⚝ template <class InputIterator> void insert(InputIterator first, InputIterator last)
▮▮▮▮⚝ void insert(std::initializer_list<value_type> il)
F14Set
的 insert()
和 emplace()
方法返回一个 std::pair
,其中 first
是指向插入位置的迭代器,second
是一个 bool
值,表示是否成功插入 (如果键已存在,则插入失败,second
为 false
)。
⚝ 查找 (Lookup):
▮▮▮▮⚝ iterator find(const Key& key)
▮▮▮▮⚝ const_iterator find(const Key& key) const
▮▮▮▮⚝ size_type count(const Key& key) const
: 返回键为 key
的元素个数 (只能是 0 或 1)。
▮▮▮▮⚝ bool contains(const Key& key) const
: Folly 提供的扩展方法,判断 F14Set
是否包含键 key
,返回 bool
值。
F14Set
没有 at()
和 operator[]
等访问值的方法,因为 F14Set
本身不存储值,只存储键。contains()
方法是更简洁的判断键是否存在的方式。
⚝ 删除 (Deletion): 与 F14Map
类似。
⚝ 容量 (Capacity): 与 F14Map
类似。
⚝ 迭代器 (Iterators): 与 F14Map
类似。
⚝ 其他操作 (Other Operations): 与 F14Map
类似。
③ 使用注意事项 (Usage Notes):
⚝ 哈希函数 (Hash Function): F14Map
和 F14Set
默认使用 folly::HashingAlgorithm<>::kCityHash64
哈希算法。如果需要自定义哈希函数,可以通过模板参数指定。自定义哈希函数需要满足哈希函数的基本要求:对于相等的键值,哈希值必须相等;哈希值应该尽可能均匀分布。
⚝ 相等比较函数 (Equality Comparison Function): 默认使用 std::equal_to<Key>
进行相等比较。如果需要自定义相等比较函数,可以通过模板参数指定。自定义相等比较函数需要满足等价关系的要求 (自反性、对称性、传递性)。
⚝ 负载因子 (Load Factor): F14Map
和 F14Set
的最大负载因子默认为 1.0。可以通过 max_load_factor()
方法获取和设置最大负载因子。较低的负载因子可以减少哈希冲突,提高性能,但会增加内存占用;较高的负载因子可以减少内存占用,但可能增加哈希冲突,降低性能。需要根据实际应用场景进行权衡。
⚝ 键的类型要求 (Requirements for Key Type): 作为 F14Map
和 F14Set
的键类型 Key
,需要满足以下要求:
▮▮▮▮⚝ 可拷贝构造 (CopyConstructible) 或 可移动构造 (MoveConstructible)。
▮▮▮▮⚝ 可默认构造 (DefaultConstructible) (可选,取决于具体操作)。
▮▮▮▮⚝ 可哈希 (Hashable): 需要提供哈希函数,可以通过重载 std::hash
模板或自定义哈希函数对象。
▮▮▮▮⚝ 可相等比较 (EqualityComparable): 需要提供相等比较运算符 operator==
或自定义相等比较函数对象。
示例代码 (F14Map 用法):
1
#include <folly/container/F14Map.h>
2
#include <iostream>
3
#include <string>
4
5
int main() {
6
folly::F14Map<int, std::string> cityMap;
7
8
// 插入元素
9
cityMap[1] = "Beijing";
10
cityMap[2] = "Shanghai";
11
cityMap.emplace(3, "Guangzhou");
12
cityMap.insert({4, "Shenzhen"});
13
14
// 查找元素
15
std::cout << "City of id 2: " << cityMap.at(2) << std::endl;
16
auto it = cityMap.find(3);
17
if (it != cityMap.end()) {
18
std::cout << "City of id 3: " << it->second << std::endl;
19
}
20
21
// 遍历元素
22
std::cout << "All cities:" << std::endl;
23
for (const auto& pair : cityMap) {
24
std::cout << "ID: " << pair.first << ", City: " << pair.second << std::endl;
25
}
26
27
// 删除元素
28
cityMap.erase(1);
29
std::cout << "Size after erase id 1: " << cityMap.size() << std::endl;
30
31
return 0;
32
}
示例代码 (F14Set 用法):
1
#include <folly/container/F14Set.h>
2
#include <iostream>
3
#include <string>
4
5
int main() {
6
folly::F14Set<std::string> citySet;
7
8
// 插入元素
9
citySet.insert("Beijing");
10
citySet.insert("Shanghai");
11
citySet.emplace("Guangzhou");
12
citySet.insert({"Shenzhen"});
13
14
// 查找元素
15
if (citySet.contains("Shanghai")) {
16
std::cout << "Shanghai is in the set." << std::endl;
17
}
18
auto it = citySet.find("Guangzhou");
19
if (it != citySet.end()) {
20
std::cout << "Found city: " << *it << std::endl;
21
}
22
23
// 遍历元素
24
std::cout << "All cities in set:" << std::endl;
25
for (const auto& city : citySet) {
26
std::cout << city << std::endl;
27
}
28
29
// 删除元素
30
citySet.erase("Beijing");
31
std::cout << "Size after erase Beijing: " << citySet.size() << std::endl;
32
33
return 0;
34
}
总而言之,F14Map
和 F14Set
提供了丰富且易于使用的 API,与 std::unordered_map
和 std::unordered_set
高度兼容,同时也提供了一些 Folly 特有的扩展方法 (如 get_ptr()
和 contains()
)。理解其 API 和用法,可以帮助开发者快速上手并高效地使用 F14 系列哈希容器,构建高性能的 C++ 应用。
3.2.3 选择合适的哈希容器:std::unordered_map vs F14Map (Choosing the Right Hash Container: std::unordered_map vs F14Map)
在 C++ 开发中,std::unordered_map
和 F14Map
都是常用的哈希容器,用于高效地存储和检索键值对。选择合适的哈希容器,需要综合考虑性能、内存占用、API 兼容性、项目依赖等因素。本节将对比 std::unordered_map
和 F14Map
的优缺点,并分析在不同场景下如何选择合适的哈希容器。
① std::unordered_map 的优缺点 (Pros and Cons of std::unordered_map):
优点 (Pros):
⚝ 标准库 (Standard Library): std::unordered_map
是 C++ 标准库的一部分,无需额外依赖,跨平台兼容性好,几乎在所有支持 C++11 或更高标准的编译器和平台上都能使用。
⚝ 广泛使用和成熟 (Widely Used and Mature): std::unordered_map
经过多年的发展和广泛应用,非常成熟和稳定,拥有庞大的用户群体和丰富的文档资源。
⚝ API 标准化 (Standardized API): std::unordered_map
的 API 是标准化的,符合 C++ 标准库的规范,易于学习和使用,与其他 STL 容器具有良好的互操作性。
⚝ 内存占用相对较低 (Relatively Low Memory Footprint): 在某些实现中,std::unordered_map
的内存占用可能略低于 F14Map
,尤其是在元素数量较少的情况下。
⚝ 迭代器失效规则明确 (Clear Iterator Invalidation Rules): std::unordered_map
的迭代器失效规则明确且可预测,方便开发者编写正确的迭代代码。
缺点 (Cons):
⚝ 性能可能不是最优 (Potentially Not Optimal Performance): std::unordered_map
的默认哈希算法和冲突解决策略可能不是在所有场景下都是最优的。在某些高负载因子或哈希冲突较多的情况下,性能可能不如 F14Map
。
⚝ 哈希算法和相等比较函数定制性有限 (Limited Customization of Hash and Equality Functions): 虽然 std::unordered_map
允许自定义哈希函数和相等比较函数,但在哈希算法的选择和优化方面,可能不如 Folly 提供的 HashingAlgorithm
灵活和丰富。
⚝ 错误提示可能不够友好 (Potentially Less User-Friendly Error Messages): 在编译错误或运行时错误方面,std::unordered_map
的错误提示可能不如 Folly 库提供的容器详细和友好。
② F14Map 的优缺点 (Pros and Cons of F14Map):
优点 (Pros):
⚝ 高性能 (High Performance): F14Map
采用了高性能的 F14 哈希算法和优化的冲突解决策略,在许多场景下,尤其是高负载因子、高并发或数据量较大的情况下,性能优于 std::unordered_map
。
⚝ 丰富的哈希算法选项 (Rich Set of Hash Algorithm Options): Folly 提供了 HashingAlgorithm
模板类,支持多种哈希算法 (如 CityHash, MurmurHash, XXHash 等),可以根据具体应用场景选择最合适的哈希算法,并进行更细粒度的性能调优。
⚝ 更好的错误提示 (Better Error Messages): Folly 库通常提供更详细、更友好的错误提示,有助于开发者快速定位和解决问题。
⚝ 与 Folly 库生态系统集成 (Integration with Folly Library Ecosystem): 如果项目已经使用了 Folly 库,或者计划引入 Folly 库,那么使用 F14Map
可以更好地融入 Folly 生态系统,并与其他 Folly 组件协同工作。
缺点 (Cons):
⚝ 非标准库 (Not Standard Library): F14Map
不是 C++ 标准库的一部分,需要额外依赖 Folly 库,增加了项目的依赖复杂性和构建难度。
⚝ 学习成本 (Learning Curve): 对于不熟悉 Folly 库的开发者,需要学习 Folly 库的基本概念和用法,存在一定的学习成本。
⚝ API 略有差异 (Slight API Differences): 虽然 F14Map
的 API 与 std::unordered_map
基本兼容,但仍然存在一些细微的差异,例如 Folly 提供的扩展方法 (get_ptr()
, contains()
等),需要注意 API 的兼容性。
⚝ 可能存在兼容性问题 (Potential Compatibility Issues): Folly 库可能在某些特定的编译器或平台上存在兼容性问题,需要进行充分的测试和验证。
⚝ 社区活跃度相对较低 (Relatively Smaller Community): 与 std::unordered_map
相比,Folly 库的社区活跃度相对较低,文档和教程资源可能不如 STL 丰富。
③ 选择建议 (Selection Recommendations):
在以下场景中,优先选择 std::unordered_map
:
⚝ 追求最大程度的跨平台兼容性,需要避免任何非标准库依赖的项目。
⚝ 项目规模较小,性能不是首要考虑因素,或者 std::unordered_map
的性能已经足够满足需求。
⚝ 团队成员对 Folly 库不熟悉,希望降低学习成本和维护成本。
⚝ 需要与其他 STL 容器和算法进行广泛的互操作。
在以下场景中,可以考虑选择 F14Map
:
⚝ 对性能有较高要求,需要尽可能榨取哈希容器的性能潜力,尤其是在高负载、高并发或大数据量的场景下。
⚝ 项目中已经使用了 Folly 库,或者计划引入 Folly 库,希望更好地融入 Folly 生态系统。
⚝ 需要更灵活的哈希算法选择和定制,例如需要尝试不同的哈希算法以找到最佳性能。
⚝ 需要更详细、更友好的错误提示,以便快速定位和解决问题。
⚝ 项目对 Folly 库的依赖没有限制,并且团队有能力维护和管理 Folly 库。
选择流程建议:
- 性能需求评估: 评估项目对哈希容器的性能需求,是否是性能瓶颈,性能提升是否能带来显著收益。
- 基准测试: 如果性能是关键因素,务必进行基准测试,对比
std::unordered_map
和F14Map
在实际应用场景下的性能表现,包括插入、查找、删除等操作的耗时、内存占用等指标。 - 依赖性评估: 评估引入 Folly 库的依赖性成本,包括构建复杂性、兼容性风险、学习成本、维护成本等。
- 团队能力评估: 评估团队成员对 Folly 库的熟悉程度,是否有能力维护和管理 Folly 库。
- 综合权衡: 综合考虑性能、依赖性、团队能力等因素,选择最合适的哈希容器。
总结:
std::unordered_map
和 F14Map
都是优秀的哈希容器,各有优缺点。std::unordered_map
胜在标准、成熟、通用,而 F14Map
胜在高性能、可定制性强。选择合适的哈希容器,需要根据具体的应用场景和项目需求进行权衡。在不确定的情况下,建议先使用 std::unordered_map
,如果性能成为瓶颈,再考虑切换到 F14Map
并进行性能测试和优化。
3.3 PackedVector:节省空间的向量容器 (PackedVector: Space-Efficient Vector Container)
介绍 PackedVector
的设计目标和使用场景,讲解如何通过位压缩技术节省内存空间,尤其适用于存储大量小整数的场景。
3.3.1 PackedVector 的位压缩原理 (Bit Packing Principle of PackedVector)
PackedVector
是 Folly 库提供的一种节省内存空间的向量容器。它的核心思想是位压缩 (bit packing),即根据实际存储数据的范围,使用尽可能少的位数来存储每个元素,从而减少内存占用。PackedVector
特别适用于存储大量小整数的场景,例如存储枚举值、小的索引值、状态标志等。
① 位压缩 (Bit Packing) 的基本概念:
传统的整数类型 (如 int
, unsigned int
, long long
) 都是固定大小的,例如 int
通常占用 4 字节 (32 位),unsigned int
也占用 4 字节。即使我们要存储的数值范围很小,例如只需要 0-7 的整数,使用 int
仍然会浪费大量的存储空间,因为 32 位可以表示的范围远大于 0-7。
位压缩 的核心思想是:不再使用固定大小的整数类型来存储数据,而是根据数据的实际范围,动态地选择合适的位数来存储每个元素。例如,对于 0-7 的整数,只需要 3 位 (23 = 8) 就足够了。如果使用 3 位来存储每个元素,那么存储 10 个这样的整数,只需要 30 位,不到 4 字节,相比于使用 int
(40 字节) 可以节省大量的内存空间。
示例说明:
假设我们要存储以下 5 个 0-7 范围的整数: [2, 5, 1, 7, 0]
。
⚝ 使用 std::vector<int>
: 每个 int
占用 4 字节 (32 位),5 个整数共占用 5 * 4 = 20 字节。
⚝ 使用 PackedVector<uint8_t, 3>
: 指定每个元素使用 3 位存储。5 个整数共占用 5 * 3 = 15 位,约 2 字节 (向上取整到字节)。相比之下,内存占用大幅减少。
② PackedVector 的实现原理:
PackedVector<IntegerType, kBits>
是 Folly 提供的模板类,其中 IntegerType
是用于存储压缩数据的底层整数类型 (通常是 uint8_t
, uint16_t
, uint32_t
, uint64_t
等无符号整数类型),kBits
是指定每个元素使用的位数。
PackedVector
的实现核心在于位域 (bit-field) 操作 和 位掩码 (bit-mask)。它将多个压缩后的元素紧凑地存储在一个或多个底层整数类型的数组中。
关键实现细节:
⚝ 位域操作: PackedVector
使用位域操作来读取和写入指定位数的元素。位域操作涉及到位移运算 (左移 <<
和右移 >>
) 和 位掩码运算 (与 &
, 或 |
, 异或 ^
等)。
⚝ 位掩码 (Bit Mask): 为了精确地操作指定位数的元素,PackedVector
会预先计算好位掩码。位掩码是一个二进制数,用于提取或设置目标位。例如,如果每个元素使用 3 位,那么位掩码可以是 0b111
(十进制 7),用于提取或设置元素的 3 位。
⚝ 跨越字节边界的处理: 当一个元素的位域跨越字节边界时 (例如,一个 3 位元素的位域一部分在字节 A 的末尾,一部分在字节 B 的开头),PackedVector
需要进行更复杂的位操作,将元素的不同部分从不同的字节中提取出来或写入进去。
⚝ 动态扩容: 类似于 std::vector
和 FBVector
,PackedVector
也支持动态扩容。当容量不足时,PackedVector
会分配更大的底层存储空间,并将原有数据复制到新的空间。扩容策略可能与 FBVector
类似,以倍数增长或按需增长。
⚝ API 兼容性: PackedVector
的 API 设计尽可能地与 std::vector
兼容,例如提供 push_back()
, emplace_back()
, operator[]
, size()
, capacity()
等常用方法,方便开发者从 std::vector
迁移到 PackedVector
。
位压缩的计算示例:
假设 PackedVector<uint8_t, 3>
,底层存储类型是 uint8_t
(8 位),每个元素使用 3 位。
⚝ 存储前 2 个元素: 第 1 个元素占用第 1 个字节的 0-2 位,第 2 个元素占用第 1 个字节的 3-5 位。
⚝ 存储第 3 个元素: 第 3 个元素占用第 1 个字节的 6-7 位,以及第 2 个字节的 0 位 (跨越字节边界)。
⚝ 存储第 4 个元素: 第 4 个元素占用第 2 个字节的 1-3 位。
⚝ 以此类推...
位操作示例 (读取元素):
假设要读取 PackedVector<uint8_t, 3>
中索引为 i
的元素。
- 计算元素在底层存储数组中的起始位位置:
bit_offset = i * kBits
。 - 计算元素所在的字节索引:
byte_index = bit_offset / 8
。 - 计算元素在字节内的位偏移:
bit_in_byte_offset = bit_offset % 8
。 - 读取字节: 从底层存储数组中读取
byte_index
和byte_index + 1
(如果元素跨越字节边界) 的字节。 - 位移和位掩码操作: 对读取的字节进行位移和位掩码操作,提取出目标元素的 3 位值。
位操作示例 (写入元素):
写入元素的过程类似,但需要更复杂的位操作,以避免覆盖同一字节中的其他元素。通常需要:
- 读取目标字节。
- 清空目标字节中要写入的位域 (使用位掩码和位与
&
运算)。 - 将要写入的值位移到正确的位置。
- 将位移后的值与清空后的字节进行位或
|
运算,写入目标字节。
③ PackedVector 的优势与局限性:
优势 (Advantages):
⚝ 节省内存空间 (Space Efficiency): PackedVector
的最大优势是显著节省内存空间,尤其是在存储大量小整数时。内存节省比例取决于每个元素使用的位数 kBits
和元素的数量。
⚝ 缓存友好性 (Cache Friendliness): 由于 PackedVector
将数据紧凑地存储,可以提高数据局部性,从而提高缓存命中率,在某些情况下可以提升性能。
局限性 (Limitations):
⚝ 性能开销 (Performance Overhead): 位压缩和解压缩操作 (位移、位掩码等) 相比于直接访问内存,会引入一定的性能开销。对于频繁读写的场景,PackedVector
的性能可能不如 std::vector
或 FBVector
。
⚝ 适用场景受限 (Limited Applicability): PackedVector
最适用于存储小范围整数,如果存储的数据范围较大,需要的位数 kBits
也会增加,内存节省效果会降低,甚至可能不如 std::vector
。对于非整数类型 (如浮点数、字符串等),PackedVector
不适用。
⚝ 实现复杂性 (Implementation Complexity): PackedVector
的实现比 std::vector
和 FBVector
更复杂,涉及到较多的位操作和边界处理。
⚝ 调试难度 (Debugging Difficulty): 位压缩后的数据不易于直接查看和调试,增加了调试难度。
总结:
PackedVector
是一种针对特定场景优化的容器,它通过位压缩技术实现了显著的内存节省,尤其适用于存储大量小整数。但位压缩也引入了性能开销和实现复杂性。在选择使用 PackedVector
时,需要权衡内存节省和性能开销,并仔细评估其是否真的适用于具体的应用场景。
3.3.2 PackedVector 的适用场景与限制 (Use Cases and Limitations of PackedVector)
PackedVector
是一种针对特定场景优化的容器,了解其适用场景和限制,可以帮助我们更好地判断是否应该在特定场景下使用 PackedVector
,以及如何正确地使用它。
① 适用场景 (Use Cases):
PackedVector
最适用于以下场景:
⚝ 存储大量小整数 (Storing Large Amounts of Small Integers): 这是 PackedVector
最典型的应用场景。当需要存储百万、千万甚至亿级别的小整数 (例如,取值范围在 0-255, 0-63, 0-7 等) 时,使用 PackedVector
可以显著节省内存空间。常见的应用包括:
▮▮▮▮⚝ 枚举值 (Enumeration Values): 存储大量的枚举值,例如状态码、类型标识等。
▮▮▮▮⚝ 小的索引值 (Small Index Values): 存储小的索引值,例如图像像素索引、网格索引等。
▮▮▮▮⚝ 状态标志 (Status Flags): 存储大量的布尔值 (可以用 1 位存储) 或小的状态标志。
▮▮▮▮⚝ 压缩数据 (Compressed Data): 作为一种数据压缩的手段,将整数数据压缩存储,例如在某些内存受限的环境中。
⚝ 内存受限环境 (Memory-Constrained Environments): 在内存资源非常宝贵的环境中 (例如,嵌入式系统、移动设备、高性能计算中的大规模数据处理等),使用 PackedVector
节省内存空间可以降低内存压力,提高程序运行效率,甚至避免内存溢出。
⚝ 缓存优化 (Cache Optimization): 在某些情况下,PackedVector
的数据紧凑性可以提高缓存命中率,从而提升性能。但这通常需要具体场景的性能测试来验证。
具体应用示例:
⚝ 图像处理 (Image Processing): 存储灰度图像的像素值 (0-255),可以使用 PackedVector<uint8_t, 8>
或 PackedVector<uint8_t, 7>
(如果灰度级数小于 256)。
⚝ 游戏开发 (Game Development): 存储游戏地图的瓦片索引 (tile index),如果瓦片种类不多,可以使用较少的位数存储索引。
⚝ 网络协议 (Network Protocol): 存储网络协议中的一些标志位、状态码等。
⚝ 大规模数据索引 (Large-Scale Data Indexing): 构建大规模数据的索引结构时,可以使用 PackedVector
存储索引值,减少索引结构的内存占用。
② 限制 (Limitations):
PackedVector
并非万能的,它也存在一些局限性,不适用于所有场景:
⚝ 性能开销 (Performance Overhead): 位压缩和解压缩操作会引入性能开销。对于频繁读写的场景,PackedVector
的性能可能不如 std::vector
或 FBVector
。如果性能是首要考虑因素,并且内存不是瓶颈,则不宜使用 PackedVector
。
⚝ 数据范围限制 (Data Range Limitation): PackedVector
只能存储整数类型的数据,并且数据范围必须足够小,才能体现出内存节省的优势。如果数据范围较大,需要的位数 kBits
也会增加,内存节省效果会降低。对于非整数类型 (如浮点数、字符串、复杂对象等),PackedVector
不适用。
⚝ 元素大小固定 (Fixed Element Size): PackedVector
在编译时就确定了每个元素使用的位数 kBits
,运行时不能动态改变。这意味着 PackedVector
只能存储固定大小的元素,不适用于存储变长数据 (例如,变长字符串)。
⚝ API 完整性 (API Completeness): 相比于 std::vector
和 FBVector
,PackedVector
的 API 可能不够完整,某些高级功能或算法可能未实现。
⚝ 调试难度 (Debugging Difficulty): 位压缩后的数据不易于直接查看和调试,增加了调试难度。
不适用场景示例:
⚝ 存储大范围整数 (Storing Large Range Integers): 例如,存储 32 位整数、64 位整数等,使用 PackedVector
可能无法节省多少内存,反而会引入性能开销。
⚝ 存储浮点数、字符串等非整数类型。
⚝ 需要频繁修改元素,且对性能要求极高的场景。
⚝ 数据量不大,内存不是瓶颈的场景。
⚝ 需要存储变长数据的场景。
③ 选择建议 (Selection Recommendations):
在选择是否使用 PackedVector
时,需要综合考虑以下因素:
⚝ 数据类型和范围: 评估要存储的数据是否是小范围整数,数据范围越小,内存节省效果越明显。
⚝ 数据量: 数据量越大,使用 PackedVector
节省的内存空间也越多。
⚝ 性能需求: 评估对读写性能的要求,如果性能是首要考虑因素,需要进行性能测试,对比 PackedVector
和其他容器的性能。
⚝ 内存限制: 评估内存资源是否紧张,如果内存资源非常宝贵,节省内存空间可能比性能更重要。
⚝ 代码复杂度和维护成本: 考虑引入 PackedVector
带来的代码复杂性和调试难度,以及团队是否具备维护 PackedVector
代码的能力。
选择流程建议:
- 分析数据特征: 分析要存储的数据类型、数据范围、数据量大小。
- 内存需求评估: 估算使用
std::vector
或FBVector
和使用PackedVector
的内存占用差异。 - 性能测试: 在实际应用场景下,进行基准测试,对比
PackedVector
和其他容器的性能,包括读写速度、内存占用等指标。 - 综合权衡: 综合考虑内存节省、性能开销、代码复杂度和维护成本,选择最合适的容器。
总结:
PackedVector
是一种非常有用的工具,可以在特定场景下显著节省内存空间。但它并非通用容器,存在性能开销和适用范围限制。只有在真正需要节省内存空间,并且数据类型和应用场景与 PackedVector
的优势相匹配时,才能发挥其最大价值。在选择使用 PackedVector
时,务必进行充分的评估和测试,确保其能带来预期的收益,并且不会引入不必要的性能问题或代码复杂性。
4. 异步编程:Futures, Promises 与协程 (Asynchronous Programming: Futures, Promises, and Coroutines)
本章深入探讨 Folly 库提供的异步编程工具,包括 Futures, Promises 和协程 (Coroutines),帮助读者构建高效、响应式的异步系统。
4.1 Futures 与 Promises:异步结果的表示与传递 (Futures and Promises: Representing and Passing Asynchronous Results)
本节详细介绍 Futures 和 Promises 的概念、作用和使用方法,讲解如何使用它们进行异步操作的结果传递和同步。
4.1.1 Promise 的创建与设置结果 (Creating Promises and Setting Results)
在异步编程中,Promise (承诺)
充当一个“承诺”未来会提供某个值的占位符。folly::Promise
是 Folly 库中用于实现这一概念的类。它允许你创建一个对象,该对象代表一个异步操作的最终结果,并且可以在操作完成时设置这个结果。
① Promise 的创建
要创建一个 folly::Promise
对象,你需要指定它将持有的值的类型。例如,如果异步操作预计返回一个 int (整型)
值,你可以这样声明一个 Promise:
1
folly::Promise<int> promise;
如果异步操作不返回任何值(void),你可以使用 folly::Promise<folly::Unit>
或简写 folly::Promise<void>
:
1
folly::Promise<void> promise_void;
② 设置 Promise 的值
当异步操作成功完成并产生结果时,你可以使用 setValue(value)
方法来设置 Promise 的值。这个值的类型必须与 Promise 声明时指定的类型相匹配。例如,对于 folly::Promise<int>
,你可以这样设置值:
1
folly::Promise<int> promise;
2
// ... 异步操作 ...
3
promise.setValue(42); // 设置 Promise 的值为 42
一旦 setValue
被调用,与这个 Promise 关联的 folly::Future (期物)
将变为 fulfilled (已完成) 状态,并且可以访问到设置的值。
③ 设置 Promise 的异常
如果异步操作在执行过程中发生错误,你应该使用 setException(exception)
方法来设置 Promise 的异常。你可以传递任何 std::exception (标准异常)
或其派生类的对象。Folly 提供了 folly::exception_wrapper (异常包装器)
来更灵活地处理异常,但通常直接使用标准异常也足够了。
1
folly::Promise<int> promise;
2
// ... 异步操作 ...
3
try {
4
// 可能会抛出异常的代码
5
throw std::runtime_error("Something went wrong");
6
} catch (const std::exception& e) {
7
promise.setException(e); // 设置 Promise 的异常
8
}
当 setException
被调用后,与 Promise 关联的 Future 将变为 rejected (已拒绝) 状态,并且尝试获取 Future 的值将会抛出设置的异常。
④ 设置 Promise 的取消状态
在某些情况下,异步操作可能被取消。你可以使用 setWith(folly::makeCancelled())
来显式地设置 Promise 为取消状态。
1
folly::Promise<int> promise;
2
// ... 异步操作,判断是否需要取消 ...
3
if (shouldCancel) {
4
promise.setWith(folly::makeCancelled()); // 设置 Promise 为取消状态
5
}
当 Promise 被设置为取消状态后,其关联的 Future 将变为 cancelled (已取消) 状态。尝试获取取消状态的 Future 的值会抛出 folly::FutureCancelled (Future已取消)
异常。
⑤ Promise 的状态转换
Promise 的状态只能单向转换,一旦设置了值、异常或取消状态,就不能再更改。Promise 的状态转换如下:
⚝ 初始状态:Promise 创建后,处于未完成状态 (unfulfilled)。
⚝ 完成状态 (fulfilled):调用 setValue
后,Promise 进入完成状态。
⚝ 拒绝状态 (rejected):调用 setException
后,Promise 进入拒绝状态。
⚝ 取消状态 (cancelled):调用 setWith(folly::makeCancelled())
后,Promise 进入取消状态。
代码示例
1
#include <folly/Future.h>
2
#include <folly/Promise.h>
3
#include <iostream>
4
#include <stdexcept>
5
6
int main() {
7
folly::Promise<int> promise;
8
folly::Future<int> future = promise.getFuture();
9
10
// 模拟异步操作成功
11
promise.setValue(100);
12
13
// 获取 Future 的结果
14
try {
15
int result = future.get();
16
std::cout << "Future result: " << result << std::endl; // 输出: Future result: 100
17
} catch (const std::exception& e) {
18
std::cerr << "Exception caught: " << e.what() << std::endl;
19
}
20
21
folly::Promise<void> void_promise;
22
folly::Future<void> void_future = void_promise.getFuture();
23
24
// 模拟异步操作失败
25
void_promise.setException(std::runtime_error("Void operation failed"));
26
27
// 获取 void Future 的结果 (会抛出异常)
28
try {
29
void_future.get();
30
} catch (const std::exception& e) {
31
std::cerr << "Void Future Exception caught: " << e.what() << std::endl; // 输出: Void Future Exception caught: Void operation failed
32
}
33
34
folly::Promise<int> cancel_promise;
35
folly::Future<int> cancel_future = cancel_promise.getFuture();
36
37
// 模拟异步操作被取消
38
cancel_promise.setWith(folly::makeCancelled());
39
40
// 获取 cancel Future 的结果 (会抛出 folly::FutureCancelled 异常)
41
try {
42
cancel_future.get();
43
std::cout << "Cancel Future result: " << cancel_future.get() << std::endl;
44
} catch (const folly::FutureCancelled& e) {
45
std::cerr << "Cancel Future Exception caught: " << e.what() << std::endl; // 输出: Cancel Future Exception caught: Future: cancelled
46
} catch (const std::exception& e) {
47
std::cerr << "Unexpected Exception caught: " << e.what() << std::endl;
48
}
49
50
return 0;
51
}
4.1.2 Future 的获取与结果等待 (Obtaining Futures and Waiting for Results)
folly::Future (期物)
代表异步操作的结果,你可以从 folly::Promise (承诺)
对象中获取与之关联的 Future。Future 允许你查询异步操作的状态,并在操作完成时获取结果(或处理异常)。
① 从 Promise 获取 Future
每个 folly::Promise
对象都有一个关联的 folly::Future
对象。你可以通过调用 promise.getFuture()
方法来获取这个 Future。一旦获取了 Future,你就可以在不同的线程或上下文中传递和操作它,而 Promise 则负责在异步操作完成时设置 Future 的状态。
1
folly::Promise<int> promise;
2
folly::Future<int> future = promise.getFuture(); // 从 promise 获取 future
② 同步等待 Future 的结果
folly::Future
提供了 get()
方法来同步地等待异步操作完成并获取结果。当调用 future.get()
时,调用线程会被阻塞,直到 Future 的状态变为 fulfilled (已完成)、rejected (已拒绝) 或 cancelled (已取消)。
⚝ 成功结果: 如果 Future 变为 fulfilled 状态,get()
方法会返回 Promise 设置的值。
⚝ 异常: 如果 Future 变为 rejected 状态,get()
方法会抛出 Promise 设置的异常。
⚝ 取消: 如果 Future 变为 cancelled 状态,get()
方法会抛出 folly::FutureCancelled
异常。
1
folly::Promise<int> promise;
2
folly::Future<int> future = promise.getFuture();
3
4
// 在另一个线程中设置 Promise 的值 (模拟异步操作完成)
5
std::thread t([&promise]() {
6
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
7
promise.setValue(123);
8
});
9
10
// 主线程同步等待 Future 的结果
11
try {
12
int result = future.get(); // 阻塞等待直到 Future 完成
13
std::cout << "Future result: " << result << std::endl; // 输出: Future result: 123
14
} catch (const std::exception& e) {
15
std::cerr << "Exception caught: " << e.what() << std::endl;
16
}
17
18
t.join();
注意: future.get()
是一个阻塞操作,应该谨慎使用,尤其是在需要保持程序响应性的场景中。在 GUI 线程或事件循环中直接调用 get()
可能会导致界面卡顿或程序无响应。
③ 异步等待 Future 的结果 (回调)
为了避免阻塞主线程,folly::Future
提供了多种方法来异步地处理结果,最常见的是使用回调函数。你可以使用 then()
、thenValue()
、thenError()
等方法来注册回调函数,当 Future 完成时,这些回调函数会被异步执行。
⚝ then(callback)
: 当 Future 完成(成功、失败或取消)时执行回调。回调函数接收一个 folly::Try<T> (尝试<T>)
对象,表示 Future 的结果,你可以检查 Try
对象的状态并获取值或异常。
⚝ thenValue(valueCallback)
: 当 Future 成功完成时执行回调。回调函数接收 Future 的值。
⚝ thenError(errorCallback)
: 当 Future 失败(rejected)时执行回调。回调函数接收 Future 的异常。
1
#include <folly/Future.h>
2
#include <folly/Promise.h>
3
#include <folly/executors/InlineExecutor.h> // 使用 InlineExecutor 简化示例
4
#include <iostream>
5
6
int main() {
7
folly::Promise<int> promise;
8
folly::Future<int> future = promise.getFuture();
9
10
// 异步注册成功回调 (thenValue)
11
future.thenValue([](int result) {
12
std::cout << "Asynchronous Future result (thenValue): " << result << std::endl;
13
}, &folly::InlineExecutor::instance()); // 使用 InlineExecutor 立即执行回调
14
15
// 异步注册完成回调 (then)
16
future.then([](folly::Try<int> tryResult) {
17
if (tryResult.hasValue()) {
18
std::cout << "Asynchronous Future result (then): " << tryResult.value() << std::endl;
19
} else if (tryResult.hasException()) {
20
std::cerr << "Asynchronous Future exception (then): " << tryResult.exception().what() << std::endl;
21
} else if (tryResult.isCancelled()) {
22
std::cerr << "Asynchronous Future cancelled (then)" << std::endl;
23
}
24
}, &folly::InlineExecutor::instance());
25
26
// 异步注册失败回调 (thenError)
27
future.thenError([](const std::exception& e) {
28
std::cerr << "Asynchronous Future error (thenError): " << e.what() << std::endl;
29
}, &folly::InlineExecutor::instance());
30
31
32
// 设置 Promise 的值,触发回调
33
promise.setValue(200);
34
35
// 注意:由于使用了 InlineExecutor,回调会立即执行。
36
// 在实际异步场景中,回调会在 Future 完成后的某个时间点在指定的 Executor 上执行。
37
38
return 0;
39
}
在这个示例中,我们使用了 folly::InlineExecutor::instance()
来作为回调的执行器。InlineExecutor (内联执行器)
会在调用 then*
方法的线程上立即执行回调,这在示例代码中简化了异步执行的复杂性。在实际应用中,你通常会使用线程池执行器或其他类型的执行器来将回调调度到不同的线程执行,以实现真正的异步处理。
4.1.3 Future 的链式操作与组合 (Chaining and Combining Futures)
folly::Future (期物)
提供了强大的链式操作和组合功能,允许你以声明式的方式处理异步流程。你可以将多个 Future 操作链接起来,形成一个异步操作管道,或者将多个 Future 组合成一个新的 Future。
① Future 的链式操作 (Chaining)
链式操作允许你将一个 Future 的结果作为输入,传递给下一个异步操作。folly::Future
提供了 then()
、map()
、flatMap()
等方法来实现链式操作。
⚝ then(callback)
: 这是最通用的链式操作。它接收一个回调函数,该函数接收前一个 Future 的 folly::Try<T>
结果,并返回一个新的 folly::Future<U>
。then()
允许你在回调函数中处理成功、失败和取消的情况,并返回不同类型的 Future。
1
folly::Future<int> future1 = ...;
2
folly::Future<std::string> future2 = future1.then([](folly::Try<int> tryResult) {
3
if (tryResult.hasValue()) {
4
int result = tryResult.value();
5
return folly::makeFuture<std::string>(std::to_string(result)); // 返回新的 Future<string>
6
} else {
7
return folly::makeFuture<std::string>(folly::exception_wrapper(tryResult.exception())); // 传播异常
8
}
9
});
⚝ map(valueCallback)
: map()
用于转换 Future 的值。它接收一个回调函数,该函数接收前一个 Future 的成功值,并返回一个新的值。map()
只能处理成功的情况,如果前一个 Future 失败,则新的 Future 也会失败并传播相同的异常。
1
folly::Future<int> future1 = ...;
2
folly::Future<std::string> future2 = future1.map([](int result) {
3
return std::to_string(result * 2); // 将 int 结果转换为 string 并翻倍
4
});
⚝ flatMap(valueToFutureCallback)
: flatMap()
类似于 map()
,但它的回调函数返回的是一个新的 folly::Future<U>
而不是一个普通的值。flatMap()
用于在异步操作链中引入新的异步操作。
1
folly::Future<int> future1 = ...;
2
folly::Future<std::string> future2 = future1.flatMap([](int result) {
3
return fetchDataFromRemoteServiceAsync(result); // 假设这是一个返回 Future<string> 的异步函数
4
});
② Future 的组合 (Combining)
组合操作允许你将多个独立的 Future 组合成一个新的 Future,以便在所有 Future 完成后执行某些操作。folly::collect()
和 folly::reduce()
是常用的 Future 组合函数。
⚝ folly::collect(futures)
: collect()
函数接收一个 Future 的容器(例如 std::vector<folly::Future<T>> (std::vector<folly::Future<T>>)
),并返回一个新的 folly::Future<std::vector<folly::Try<T>>> (folly::Future<std::vector<folly::Try<T>>>)
。当输入的所有 Future 都完成时,新的 Future 才会完成。新的 Future 的结果是一个 std::vector
,其中包含了每个输入 Future 的 folly::Try
结果。
1
std::vector<folly::Future<int>> futures = {future1, future2, future3};
2
folly::Future<std::vector<folly::Try<int>>> combinedFuture = folly::collect(futures);
3
combinedFuture.thenValue([](const std::vector<folly::Try<int>>& results) {
4
for (const auto& tryResult : results) {
5
if (tryResult.hasValue()) {
6
std::cout << "Collected result: " << tryResult.value() << std::endl;
7
} else if (tryResult.hasException()) {
8
std::cerr << "Collected exception: " << tryResult.exception().what() << std::endl;
9
}
10
}
11
}, &folly::InlineExecutor::instance());
⚝ folly::reduce(futures, initialValue, reduceFunction)
: reduce()
函数也接收一个 Future 的容器,以及一个初始值和一个归约函数。它将归约函数应用于每个 Future 的结果和累积值,最终返回一个新的 Future,其结果是最终的累积值。
1
std::vector<folly::Future<int>> futures = {future1, future2, future3};
2
folly::Future<int> reducedFuture = folly::reduce(futures, 0, [](int accumulated, folly::Try<int> tryResult) {
3
if (tryResult.hasValue()) {
4
return accumulated + tryResult.value();
5
} else {
6
return accumulated; // 忽略失败的 Future
7
}
8
});
9
reducedFuture.thenValue([](int finalSum) {
10
std::cout << "Reduced sum: " << finalSum << std::endl;
11
}, &folly::InlineExecutor::instance());
代码示例:Future 链式操作
1
#include <folly/Future.h>
2
#include <folly/Promise.h>
3
#include <folly/executors/InlineExecutor.h>
4
#include <iostream>
5
#include <string>
6
7
folly::Future<int> asyncOperation1() {
8
folly::Promise<int> promise;
9
std::thread t([promise = std::move(promise)]() {
10
std::this_thread::sleep_for(std::chrono::milliseconds(500));
11
promise.setValue(10);
12
});
13
t.detach();
14
return promise.getFuture();
15
}
16
17
folly::Future<std::string> asyncOperation2(int input) {
18
folly::Promise<std::string> promise;
19
std::thread t([promise = std::move(promise), input]() {
20
std::this_thread::sleep_for(std::chrono::milliseconds(300));
21
promise.setValue("Result: " + std::to_string(input * 2));
22
});
23
t.detach();
24
return promise.getFuture();
25
}
26
27
int main() {
28
asyncOperation1()
29
.flatMap(asyncOperation2) // 链式调用 flatMap
30
.thenValue([](const std::string& finalResult) {
31
std::cout << "Final Result: " << finalResult << std::endl; // 输出: Final Result: Result: 20
32
}, &folly::InlineExecutor::instance())
33
.thenError([](const std::exception& e) {
34
std::cerr << "Error in chain: " << e.what() << std::endl;
35
}, &folly::InlineExecutor::instance());
36
37
// 为了防止程序过早退出,等待一段时间,让异步操作完成
38
std::this_thread::sleep_for(std::chrono::seconds(2));
39
return 0;
40
}
4.2 Executors:任务调度与执行 (Executors: Task Scheduling and Execution)
本节介绍 Folly 库的 Executor (执行器) 框架,包括不同类型的 Executor (ThreadPoolExecutor (线程池执行器)
, InlineExecutor (内联执行器)
等) 以及如何使用 Executor 调度和执行异步任务。
4.2.1 Executor 接口与不同类型的 Executor (Executor Interface and Different Executor Types)
folly::Executor (执行器)
是一个抽象接口,用于定义任务的执行策略。它负责调度和执行提交给它的任务,而任务的具体执行方式(例如,在哪个线程上执行)由 Executor 的具体实现决定。
① Executor 接口
folly::Executor
接口定义了执行任务的基本方法,最核心的是 execute(Func func)
方法。Func
是一个可调用对象(例如,函数指针、Lambda 表达式、函数对象),表示要执行的任务。
1
class Executor {
2
public:
3
virtual ~Executor() = default;
4
virtual void execute(Func func) = 0; // 纯虚函数,需要子类实现
5
6
// ... 其他方法,例如 schedule, keepAlive 等 ...
7
};
execute(Func func)
方法接收一个函数对象 func
,Executor 的实现负责在适当的时候和地点调用 func()
来执行任务。
② 常见的 Executor 类型
Folly 提供了多种 Executor 的实现,以满足不同的并发和执行策略需求。
⚝ InlineExecutor (内联执行器)
: InlineExecutor
是最简单的 Executor。它在调用 execute()
方法的线程上 立即 执行提交的任务。InlineExecutor
主要用于测试、同步回调或当任务非常轻量且不希望切换线程时。
1
folly::InlineExecutor inlineExecutor;
2
inlineExecutor.execute([]() {
3
std::cout << "Task executed inline" << std::endl; // 立即执行
4
});
⚝ ThreadPoolExecutor (线程池执行器)
: ThreadPoolExecutor
使用一个线程池来执行任务。它维护一组工作线程,并将提交的任务分派给这些线程执行。ThreadPoolExecutor
是最常用的 Executor,适用于大多数并发场景,可以有效地利用多核 CPU 资源。你可以配置线程池的大小、线程的最大空闲时间等参数。
1
// 创建一个固定大小为 4 的线程池
2
auto threadPoolExecutor = std::make_shared<folly::ThreadPoolExecutor>(4);
3
threadPoolExecutor->execute([]() {
4
std::cout << "Task executed in thread pool" << std::endl; // 在线程池线程中执行
5
});
⚝ IOThreadPoolExecutor (IO线程池执行器)
: IOThreadPoolExecutor
专门为 I/O 密集型任务设计。它与 ThreadPoolExecutor
类似,也使用线程池,但针对 I/O 操作进行了优化。在处理网络请求、文件 I/O 等场景中,IOThreadPoolExecutor
可能更高效。
1
auto ioThreadPoolExecutor = std::make_shared<folly::IOThreadPoolExecutor>(2);
2
ioThreadPoolExecutor->execute([]() {
3
// 执行 I/O 密集型任务
4
std::cout << "IO task executed in thread pool" << std::endl;
5
});
⚝ CPUThreadPoolExecutor (CPU线程池执行器)
: CPUThreadPoolExecutor
是为 CPU 密集型任务优化的线程池执行器。它通常用于执行计算密集型任务,并可以根据 CPU 核心数进行自动配置。
1
auto cpuThreadPoolExecutor = std::make_shared<folly::CPUThreadPoolExecutor>(std::thread::hardware_concurrency()); // 根据 CPU 核心数自动配置
2
cpuThreadPoolExecutor->execute([]() {
3
// 执行 CPU 密集型任务
4
std::cout << "CPU task executed in thread pool" << std::endl;
5
});
⚝ EventBaseExecutor (EventBase执行器)
: EventBaseExecutor
将任务调度到指定的 folly::EventBase (事件基类)
的事件循环中执行。EventBase
是 Folly 库中用于事件驱动编程的核心组件,EventBaseExecutor
允许你将异步任务与事件循环集成,常用于网络编程和 GUI 编程。
1
folly::EventBase eventBase;
2
folly::EventBaseExecutor eventBaseExecutor(&eventBase);
3
eventBaseExecutor.execute([]() {
4
std::cout << "Task executed in EventBase event loop" << std::endl; // 在 EventBase 事件循环中执行
5
});
6
eventBase.loop(); // 启动 EventBase 事件循环
③ 选择合适的 Executor
选择合适的 Executor 取决于你的应用场景和任务类型:
⚝ 轻量级同步任务或测试: InlineExecutor
⚝ CPU 密集型任务: CPUThreadPoolExecutor
⚝ I/O 密集型任务: IOThreadPoolExecutor
⚝ 通用并发任务: ThreadPoolExecutor
⚝ 事件驱动编程: EventBaseExecutor
你可以根据具体需求选择 Folly 提供的 Executor 类型,或者实现自定义的 Executor 来满足特定的执行策略。
4.2.2 使用 Executor 调度异步任务 (Scheduling Asynchronous Tasks with Executors)
folly::Executor (执行器)
提供了多种方法来提交和调度异步任务,包括 execute()
、add()
和 schedule()
。
① execute(Func func)
execute(Func func)
是 folly::Executor
接口中最基本的方法。它接收一个函数对象 func
,并将任务提交给 Executor 执行。Executor 会根据自身的执行策略来调度和执行任务。execute()
方法通常是异步的,调用后立即返回,任务会在稍后的某个时间点执行。
1
auto threadPoolExecutor = std::make_shared<folly::ThreadPoolExecutor>(4);
2
threadPoolExecutor->execute([]() {
3
std::cout << "Executing task using execute()" << std::endl;
4
});
② add(Func func)
add(Func func)
方法与 execute()
方法功能类似,也是将任务提交给 Executor 执行。在大多数情况下,add()
和 execute()
可以互换使用。在 Folly 内部,add()
方法在某些 Executor 实现中可能会有细微的优化或行为差异,但在通常的使用场景下,它们的作用是相同的。
1
auto threadPoolExecutor = std::make_shared<folly::ThreadPoolExecutor>(4);
2
threadPoolExecutor->add([]() {
3
std::cout << "Executing task using add()" << std::endl;
4
});
③ schedule(Func func, std::chrono::milliseconds delay)
schedule(Func func, std::chrono::milliseconds delay)
方法允许你延迟执行任务。它接收一个函数对象 func
和一个延迟时间 delay
,Executor 会在指定的延迟时间后调度和执行任务。schedule()
方法在需要定时任务或延迟操作时非常有用。
1
auto threadPoolExecutor = std::make_shared<folly::ThreadPoolExecutor>(4);
2
threadPoolExecutor->schedule([]() {
3
std::cout << "Executing delayed task after 1 second" << std::endl;
4
}, std::chrono::milliseconds(1000)); // 延迟 1 秒执行
④ 将 Executor 与 Future 结合使用
Executor 通常与 folly::Future (期物)
结合使用,以实现更复杂的异步流程控制。folly::via(Executor* executor)
函数可以将 Future 的后续操作调度到指定的 Executor 上执行。
1
folly::Future<int> asyncOperation() {
2
folly::Promise<int> promise;
3
auto threadPoolExecutor = std::make_shared<folly::ThreadPoolExecutor>(1);
4
threadPoolExecutor->execute([promise = std::move(promise)]() {
5
std::this_thread::sleep_for(std::chrono::milliseconds(500));
6
promise.setValue(100);
7
});
8
return promise.getFuture();
9
}
10
11
int main() {
12
auto ioThreadPoolExecutor = std::make_shared<folly::IOThreadPoolExecutor>(2);
13
asyncOperation()
14
.via(ioThreadPoolExecutor.get()) // 将后续操作调度到 IOThreadPoolExecutor 上
15
.thenValue([](int result) {
16
std::cout << "Future result processed in IO thread pool: " << result << std::endl;
17
}, &folly::InlineExecutor::instance()); // 回调在 InlineExecutor 上执行
18
19
// 等待一段时间,让异步操作完成
20
std::this_thread::sleep_for(std::chrono::seconds(2));
21
return 0;
22
}
在这个例子中,asyncOperation()
函数返回的 Future 的后续 thenValue()
操作通过 .via(ioThreadPoolExecutor.get())
被调度到 ioThreadPoolExecutor
上执行。这意味着 thenValue()
中的回调函数会在 ioThreadPoolExecutor
的线程池中的线程上执行,而不是在调用 thenValue()
的线程上执行。这允许你控制异步操作链中每个环节的执行线程,以优化性能和资源利用率。
4.2.3 自定义 Executor 的实现 (Implementing Custom Executors)
虽然 Folly 提供了多种内置的 Executor 实现,但在某些特殊场景下,你可能需要实现自定义的 Executor 来满足特定的需求,例如优先级调度、资源限制、任务监控等。
① 创建自定义 Executor 类
要创建自定义 Executor,你需要继承 folly::Executor
抽象类,并实现 execute(Func func)
纯虚函数。你还需要考虑线程安全性和生命周期管理。
1
#include <folly/Executor.h>
2
#include <iostream>
3
#include <thread>
4
#include <mutex>
5
#include <condition_variable>
6
#include <deque>
7
8
class PriorityExecutor : public folly::Executor {
9
public:
10
PriorityExecutor(int threadCount) : running_(true) {
11
for (int i = 0; i < threadCount; ++i) {
12
threads_.emplace_back([this] {
13
while (running_) {
14
std::function<void()> task;
15
{
16
std::unique_lock<std::mutex> lock(mutex_);
17
cv_.wait(lock, [this] { return !taskQueue_.empty() || !running_; });
18
if (!taskQueue_.empty()) {
19
task = std::move(taskQueue_.front());
20
taskQueue_.pop_front();
21
} else if (!running_) {
22
break; // 线程退出
23
} else {
24
continue; // 继续等待
25
}
26
}
27
if (task) {
28
task(); // 执行任务
29
}
30
}
31
});
32
}
33
}
34
35
~PriorityExecutor() override {
36
{
37
std::lock_guard<std::mutex> lock(mutex_);
38
running_ = false; // 设置停止标志
39
}
40
cv_.notify_all(); // 通知所有等待线程退出
41
for (auto& thread : threads_) {
42
thread.join(); // 等待所有线程结束
43
}
44
}
45
46
void execute(folly::Func func) override {
47
{
48
std::lock_guard<std::mutex> lock(mutex_);
49
taskQueue_.emplace_back(std::move(func)); // 将任务添加到队列
50
}
51
cv_.notify_one(); // 通知一个等待线程
52
}
53
54
private:
55
std::vector<std::thread> threads_;
56
std::mutex mutex_;
57
std::condition_variable cv_;
58
std::deque<std::function<void()>> taskQueue_; // 任务队列
59
bool running_;
60
};
② 实现 execute(Func func)
方法
在 execute(Func func)
方法中,你需要定义任务的执行逻辑。这可能包括将任务添加到队列、调度任务到线程池、提交任务到事件循环等。在上面的 PriorityExecutor
示例中,我们将任务添加到一个双端队列 taskQueue_
,并使用条件变量 cv_
通知等待的线程来执行任务。
③ 考虑线程安全性和同步
自定义 Executor 需要考虑线程安全性。如果 Executor 需要在多个线程中访问共享状态(例如任务队列),你需要使用互斥锁、条件变量或其他同步机制来保护共享状态,避免数据竞争。在 PriorityExecutor
示例中,我们使用了 std::mutex
和 std::condition_variable
来同步对任务队列的访问。
④ 生命周期管理
自定义 Executor 需要妥善管理其生命周期,包括线程的创建和销毁、资源的释放等。在 PriorityExecutor
的析构函数中,我们设置了 running_
标志为 false,并通知所有等待线程退出,然后等待所有线程结束,以确保 Executor 在销毁时能够安全地释放资源。
⑤ 使用自定义 Executor
创建自定义 Executor 后,你可以像使用内置 Executor 一样使用它,例如将其传递给 folly::via()
函数或直接调用其 execute()
方法。
1
int main() {
2
auto priorityExecutor = std::make_shared<PriorityExecutor>(2); // 创建自定义的 PriorityExecutor,线程数为 2
3
priorityExecutor->execute([]() {
4
std::cout << "Task executed in PriorityExecutor" << std::endl; // 使用自定义 Executor 执行任务
5
});
6
7
std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待任务执行完成
8
return 0;
9
}
自定义 Executor 的实现可能比较复杂,需要深入理解并发编程和同步机制。在大多数情况下,Folly 提供的内置 Executor 已经足够满足需求。只有在非常特殊的情况下,才需要考虑实现自定义 Executor。
4.3 协程 (Coroutines):简化异步代码的编写 (Coroutines: Simplifying Asynchronous Code Writing)
本节介绍 Folly 库的协程 (Coroutines) 支持,包括 co_await (协程等待)
, co_return (协程返回)
, co_yield (协程产出)
等关键字的使用,以及如何使用协程简化异步代码的编写和维护。
4.3.1 协程的基本概念与关键字 (Basic Concepts and Keywords of Coroutines)
协程 (Coroutines) 是一种轻量级的并发编程模型,它允许你以同步的、顺序的代码风格编写异步代码,从而提高代码的可读性和可维护性。Folly 库提供了对 C++20 协程的支持,并提供了一些额外的工具来简化协程的使用。
① 协程的概念
⚝ 顺序代码风格: 协程允许你像编写同步代码一样编写异步代码。你不需要显式地处理回调函数、Promise 或 Future,而是可以使用顺序的控制流语句(例如 if
、for
、while
)来描述异步逻辑。
⚝ 轻量级: 协程的上下文切换开销非常小,通常比线程的上下文切换开销低得多。这使得协程非常适合处理高并发、低延迟的异步任务。
⚝ 可暂停和恢复: 协程可以在执行过程中暂停 (suspend) 和恢复 (resume)。当协程遇到一个异步操作时,它可以暂停执行,将控制权交还给调度器,并在异步操作完成后从暂停点恢复执行。
② 协程关键字
Folly 协程基于 C++20 标准协程,并使用了以下关键字:
⚝ co_await (协程等待)
: co_await
关键字用于暂停协程的执行,等待一个 awaitable (可等待) 对象完成。awaitable 对象通常是 folly::Future (期物)
或其他表示异步操作的对象。当 awaitable 对象完成时,协程会从暂停点恢复执行,并获取 awaitable 对象的结果。
1
folly::coro::Task<int> myCoroutine() {
2
folly::Future<int> future = asyncOperation();
3
int result = co_await future; // 暂停协程,等待 future 完成,获取结果
4
co_return result * 2; // 返回协程结果
5
}
⚝ co_return (协程返回)
: co_return
关键字用于从协程返回值,并结束协程的执行。co_return
的返回值类型必须与协程声明时指定的返回类型相匹配。对于返回 folly::coro::Task<void> (folly::coro::Task<void>)
的协程,可以使用 co_return;
返回。
1
folly::coro::Task<int> myCoroutine(int value) {
2
co_return value + 10; // 返回协程结果
3
}
⚝ co_yield (协程产出)
: co_yield
关键字用于在生成器协程中产出一个值,并将协程暂停。生成器协程可以多次 co_yield
值,每次 co_yield
都会暂停协程并将值返回给调用者。当调用者请求下一个值时,协程会从上次暂停点恢复执行。co_yield
通常用于实现迭代器或数据流。
1
folly::coro::Generator<int> myGenerator() {
2
for (int i = 0; i < 5; ++i) {
3
co_yield i; // 产出值 i,暂停协程
4
}
5
co_return; // 生成器结束
6
}
③ 协程的返回类型
Folly 协程通常使用以下返回类型:
⚝ folly::coro::Task<T> (folly::coro::Task<T>)
: 用于表示返回类型为 T
的协程。folly::coro::Task
类似于 folly::Future
,但它是专为协程设计的。你可以使用 co_await
等待 folly::coro::Task
完成,并获取其结果。
⚝ folly::coro::Task<void> (folly::coro::Task<void>)
: 用于表示不返回任何值的协程(类似于 void
返回类型的函数)。
⚝ folly::coro::Generator<T> (folly::coro::Generator<T>)
: 用于表示生成器协程,它可以产出多个类型为 T
的值。
④ 协程的启动和执行
要启动和执行协程,你需要调用协程函数并获取其返回的 folly::coro::Task
或 folly::coro::Generator
对象。然后,你可以使用 co_await
等待 Task 完成,或使用迭代器遍历 Generator 产出的值。
1
folly::coro::Task<int> myCoroutine() { /* ... */ }
2
folly::coro::Generator<int> myGenerator() { /* ... */ }
3
4
int main() {
5
// 启动 Task 协程并等待结果
6
folly::coro::run([]() -> folly::coro::Task<void> {
7
int result = co_await myCoroutine();
8
std::cout << "Coroutine result: " << result << std::endl;
9
co_return;
10
});
11
12
// 遍历 Generator 协程产出的值
13
for (int value : myGenerator()) {
14
std::cout << "Generator value: " << value << std::endl;
15
}
16
17
return 0;
18
}
folly::coro::run()
函数用于启动一个顶层协程,并等待其完成。对于 Generator 协程,你可以直接使用范围 for 循环遍历其产出的值。
4.3.2 使用协程编写异步函数 (Writing Asynchronous Functions with Coroutines)
协程可以显著简化异步代码的编写。使用 co_await
关键字,你可以将异步操作的结果以同步的方式获取,从而避免了回调地狱和复杂的 Future 链。
① 将 Future 转换为协程
假设你有一个返回 folly::Future (期物)
的异步函数 asyncOperation() (异步操作函数)
:
1
folly::Future<int> asyncOperation() {
2
folly::Promise<int> promise;
3
std::thread t([promise = std::move(promise)]() {
4
std::this_thread::sleep_for(std::chrono::milliseconds(500));
5
promise.setValue(100);
6
});
7
t.detach();
8
return promise.getFuture();
9
}
你可以将其转换为协程函数 coroutineOperation() (协程操作函数)
,使用 co_await
等待 asyncOperation()
的结果:
1
folly::coro::Task<int> coroutineOperation() {
2
int result = co_await asyncOperation(); // 使用 co_await 等待 Future 完成
3
co_return result * 2; // 返回协程结果
4
}
在 coroutineOperation()
中,co_await asyncOperation()
会暂停协程的执行,直到 asyncOperation()
返回的 Future 完成。当 Future 完成时,协程会恢复执行,并将 Future 的结果赋值给 result
变量。整个异步流程在协程中看起来就像同步代码一样。
② 复杂的异步流程控制
协程可以简化复杂的异步流程控制,例如顺序执行、并行执行、条件执行、循环执行等。
⚝ 顺序执行: 多个异步操作顺序执行,可以使用多个 co_await
语句串联起来。
1
folly::coro::Task<int> sequentialOperations() {
2
int result1 = co_await asyncOperation1();
3
int result2 = co_await asyncOperation2(result1);
4
int result3 = co_await asyncOperation3(result2);
5
co_return result3;
6
}
⚝ 并行执行: 多个异步操作并行执行,可以使用 folly::collect()
函数组合多个 Future,然后 co_await
组合后的 Future。
1
folly::coro::Task<std::vector<int>> parallelOperations() {
2
auto future1 = asyncOperation1();
3
auto future2 = asyncOperation2();
4
auto future3 = asyncOperation3();
5
std::vector<folly::Future<int>> futures = {future1, future2, future3};
6
std::vector<folly::Try<int>> results = co_await folly::collect(futures); // 并行等待所有 Future 完成
7
std::vector<int> values;
8
for (const auto& tryResult : results) {
9
if (tryResult.hasValue()) {
10
values.push_back(tryResult.value());
11
} else {
12
// 处理异常
13
}
14
}
15
co_return values;
16
}
⚝ 条件执行: 根据异步操作的结果,条件性地执行后续操作,可以使用 if
语句和 co_await
结合。
1
folly::coro::Task<std::string> conditionalOperation() {
2
int result = co_await asyncOperation1();
3
if (result > 0) {
4
std::string strResult = co_await asyncOperation2(result);
5
co_return strResult;
6
} else {
7
co_return "Result is not positive";
8
}
9
}
⚝ 循环执行: 循环执行异步操作,可以使用 for
或 while
循环和 co_await
结合。
1
folly::coro::Task<int> loopOperations(int count) {
2
int sum = 0;
3
for (int i = 0; i < count; ++i) {
4
int result = co_await asyncOperation();
5
sum += result;
6
}
7
co_return sum;
8
}
③ 代码示例:使用协程简化异步 HTTP 请求
假设你需要使用 Folly 的 AsyncSocket (异步套接字)
发送 HTTP 请求并接收响应。使用协程可以简化异步 HTTP 客户端的编写。
1
#include <folly/coro/Task.h>
2
#include <folly/coro/ রান.h>
3
#include <folly/io/async/AsyncSocket.h>
4
#include <folly/io/async/EventBase.h>
5
#include <folly/String.h>
6
#include <iostream>
7
8
folly::coro::Task<folly::fbstring> fetchHttpContentCoro(folly::EventBase& evb, const std::string& host, int port, const std::string& path) {
9
auto socket = folly::AsyncSocket::UniquePtr(new folly::AsyncSocket(&evb));
10
co_await socket->connect(folly::SocketAddress(host, port)); // co_await 连接
11
12
folly::fbstring request = folly::fbstringPrintf("GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", path.c_str(), host.c_str());
13
co_await socket->write(request); // co_await 发送请求
14
15
folly::fbstring response;
16
while (true) {
17
auto readResult = co_await socket->read(); // co_await 读取数据
18
if (readResult <= 0) {
19
break; // 连接关闭或错误
20
}
21
response += readResult.move();
22
}
23
co_return response;
24
}
25
26
int main() {
27
folly::EventBase eventBase;
28
folly::coro::run([&]() -> folly::coro::Task<void> {
29
try {
30
folly::fbstring content = co_await fetchHttpContentCoro(eventBase, "www.example.com", 80, "/"); // co_await 获取 HTTP 内容
31
std::cout << "HTTP Content:\n" << content.toStdString() << std::endl;
32
} catch (const std::exception& e) {
33
std::cerr << "Exception: " << e.what() << std::endl;
34
}
35
co_return;
36
});
37
eventBase.loop();
38
return 0;
39
}
在这个示例中,fetchHttpContentCoro()
函数使用协程和 co_await
关键字,以同步的代码风格实现了异步 HTTP 请求的发送和响应的接收。代码逻辑清晰易懂,避免了传统回调方式的复杂性。
4.3.3 协程的错误处理与取消 (Error Handling and Cancellation in Coroutines)
协程提供了自然的错误处理和取消机制,可以使异步程序的健壮性更高。
① 错误处理
协程中的错误处理与同步代码类似,可以使用 try-catch (尝试-捕获)
块来捕获和处理异常。当在 co_await
表达式中等待的 Future 变为 rejected (已拒绝) 状态时,Future 中设置的异常会被抛出,你可以在 try-catch
块中捕获并处理这个异常。
1
folly::coro::Task<int> myCoroutine() {
2
try {
3
int result = co_await asyncOperation(); // 可能会抛出异常的异步操作
4
co_return result * 2;
5
} catch (const std::exception& e) {
6
std::cerr << "Coroutine caught exception: " << e.what() << std::endl;
7
co_return -1; // 返回错误码
8
}
9
}
如果 asyncOperation()
返回的 Future 变为 rejected 状态,co_await asyncOperation()
会抛出 Future 中设置的异常。catch
块会捕获这个异常,并进行相应的错误处理,例如记录日志、返回错误码或抛出新的异常。
② 取消
Folly 协程支持取消操作。你可以通过 folly::CancellationToken (取消令牌)
和 folly::CancellationTokenSource (取消令牌源)
来实现协程的取消。
⚝ folly::CancellationTokenSource (取消令牌源)
: 用于创建和控制取消令牌。你可以使用 CancellationTokenSource
的 get_token() (获取令牌)
方法获取一个 folly::CancellationToken
对象,并使用 cancel() (取消)
方法触发取消操作。
⚝ folly::CancellationToken (取消令牌)
: 用于传递取消信号。你可以将 CancellationToken
对象传递给协程函数,并在协程中使用 co_await folly::coro::co_checkForCancellation(cancellationToken) (协程等待取消检查)
检查是否收到取消信号。
1
#include <folly/coro/Task.h>
2
#include <folly/coro/ রান.h>
3
#include <folly/CancellationToken.h>
4
#include <iostream>
5
6
folly::coro::Task<int> cancellableCoroutine(folly::CancellationToken cancellationToken) {
7
for (int i = 0; i < 10; ++i) {
8
co_await folly::coro::co_checkForCancellation(cancellationToken); // 检查取消信号
9
std::cout << "Coroutine running: " << i << std::endl;
10
std::this_thread::sleep_for(std::chrono::milliseconds(200));
11
}
12
co_return 100;
13
}
14
15
int main() {
16
folly::CancellationTokenSource cts;
17
folly::CancellationToken cancellationToken = cts.get_token();
18
19
folly::coro::run([&]() -> folly::coro::Task<void> {
20
folly::coro::Task<int> task = cancellableCoroutine(cancellationToken);
21
22
// 延迟 1 秒后取消协程
23
folly::EventBase evb;
24
evb.timer().scheduleOnce([&]() {
25
cts.cancel(); // 触发取消
26
std::cout << "Coroutine cancelled" << std::endl;
27
}, std::chrono::seconds(1));
28
evb.loop();
29
30
try {
31
int result = co_await std::move(task); // 等待协程完成
32
std::cout << "Coroutine result: " << result << std::endl; // 如果协程被取消,不会执行到这里
33
} catch (const folly::FutureCancelled& e) {
34
std::cerr << "Coroutine cancelled exception caught: " << e.what() << std::endl; // 输出取消异常
35
}
36
co_return;
37
});
38
39
return 0;
40
}
在 cancellableCoroutine()
函数中,co_await folly::coro::co_checkForCancellation(cancellationToken)
会定期检查 cancellationToken
是否被取消。如果取消令牌被触发,co_checkForCancellation()
会抛出 folly::FutureCancelled
异常,协程可以通过 try-catch
块捕获这个异常并进行清理操作。在 main()
函数中,我们创建了一个 CancellationTokenSource
,并在 1 秒后触发取消操作。协程在被取消后,会抛出 folly::FutureCancelled
异常,并在 catch
块中被捕获。
通过结合错误处理和取消机制,协程可以更好地处理异步操作中的各种异常情况,提高异步程序的可靠性和健壮性。
5. 网络编程:AsyncSocket 与 EventBase (Network Programming: AsyncSocket and EventBase)
本章深入探讨 Folly 库提供的网络编程组件,包括 AsyncSocket
和 EventBase
,帮助读者构建高性能、可扩展的网络应用。
5.1 EventBase:事件循环与 I/O 多路复用 (EventBase: Event Loop and I/O Multiplexing)
详细介绍 EventBase
的作用、原理和使用方法,讲解如何使用 EventBase
进行事件驱动的编程和 I/O 多路复用。
5.1.1 EventBase 的事件循环机制 (Event Loop Mechanism of EventBase)
深入剖析 EventBase
的事件循环模型,包括事件注册、事件分发和事件处理流程。
EventBase
是 Folly 库中进行事件驱动编程的核心组件,它实现了事件循环 (event loop) 机制,并提供了 I/O 多路复用 (I/O multiplexing) 的能力。事件循环是异步编程的基础,它允许程序在等待外部事件(如网络数据到达、定时器到期等)时,仍然可以执行其他任务,从而提高程序的并发性和响应性。
① 事件循环 (Event Loop) 的基本概念
事件循环可以被看作是一个不断重复以下三个步骤的循环:
▮▮▮▮ⓐ 事件注册 (Event Registration):程序将感兴趣的事件(例如,文件描述符上的读写事件、定时器事件等)注册到 EventBase
中。每个事件都关联一个事件处理器 (event handler),也称为回调函数 (callback function)。
▮▮▮▮ⓑ 事件等待与分发 (Event Wait and Dispatch):EventBase
监控所有已注册的事件源。当某个事件发生时(例如,文件描述符变得可读或可写,定时器到期),EventBase
会检测到这个事件,并将其分发给相应的事件处理器。这个过程通常使用高效的 I/O 多路复用机制,如 epoll
(Linux)、kqueue
(macOS, FreeBSD) 或 select
/poll
(跨平台)。
▮▮▮▮ⓒ 事件处理 (Event Handling):事件处理器被调用,执行与事件相关的任务。例如,如果是一个文件描述符上的读事件,事件处理器会读取数据;如果是一个定时器事件,事件处理器会执行定时任务。事件处理完成后,事件循环再次回到事件等待与分发步骤,继续监控新的事件。
② EventBase 的核心组件
EventBase
的实现涉及到以下几个关键组件:
▮▮▮▮ⓐ 事件队列 (Event Queue):EventBase
内部维护一个事件队列,用于存储待处理的事件。当事件发生时,会被添加到队列中,等待事件循环处理。
▮▮▮▮ⓑ 事件多路复用器 (Event Multiplexer):这是 EventBase
的核心,负责监控文件描述符上的 I/O 事件。EventBase
默认使用系统中最高效的 I/O 多路复用机制。常见的实现包括:
▮▮▮▮⚝ epoll
:Linux 系统上高效的 I/O 多路复用接口,支持边缘触发 (edge-triggered) 和水平触发 (level-triggered) 模式。
▮▮▮▮⚝ kqueue
:FreeBSD 和 macOS 系统上的高效 I/O 多路复用接口,功能强大且灵活。
▮▮▮▮⚝ select
/poll
:跨平台的 I/O 多路复用接口,但效率相对较低,通常作为备选方案。
▮▮▮▮ⓒ 定时器 (Timer):EventBase
提供了定时器功能,允许程序注册定时事件,在指定的时间或周期性地执行任务。定时器通常使用最小堆 (min-heap) 等数据结构高效管理。
▮▮▮▮ⓓ 事件处理器注册表 (Event Handler Registry):EventBase
内部维护一个注册表,记录了每个事件源和其对应的事件处理器。当事件发生时,EventBase
可以根据注册表找到并调用正确的事件处理器。
③ EventBase 的工作流程
EventBase
的典型工作流程如下:
▮▮▮▮ⓐ 创建 EventBase 对象:首先,需要创建一个 folly::EventBase
对象,它是事件循环的载体。
1
#include <folly/io/async/EventBase.h>
2
3
folly::EventBase evb;
▮▮▮▮ⓑ 注册事件源和事件处理器:将需要监控的事件源(例如,文件描述符)和对应的事件处理器注册到 EventBase
中。例如,可以使用 EventBase::addHandler()
注册文件描述符的读写事件处理器,或使用 EventBase::runAfterDelay()
注册定时器事件。
▮▮▮▮ⓒ 启动事件循环:调用 EventBase::loop()
或 EventBase::loopForever()
启动事件循环。loop()
会处理队列中的事件,直到队列为空或达到指定的循环次数,而 loopForever()
会一直运行,直到显式退出。
1
evb.loopForever(); // 启动事件循环,无限期运行
▮▮▮▮ⓓ 事件处理与回调:当注册的事件发生时,EventBase
会调用相应的事件处理器(回调函数)。在事件处理器中,程序可以执行相应的业务逻辑,例如读取或写入数据,处理定时任务等。
▮▮▮▮ⓔ 退出事件循环:在适当的时候,可以调用 EventBase::terminateLoopSoon()
或 EventBase::terminateLoopNow()
来请求事件循环退出。terminateLoopSoon()
会在当前事件处理完成后退出,而 terminateLoopNow()
会立即退出。
④ 示例:简单的 EventBase 使用
以下是一个简单的示例,演示如何使用 EventBase
注册一个定时器事件,并在定时器到期时打印一条消息。
1
#include <folly/io/async/EventBase.h>
2
#include <folly/io/async/TimeoutManager.h>
3
#include <iostream>
4
5
int main() {
6
folly::EventBase evb;
7
8
// 定义定时器回调函数
9
auto timerCallback = []() {
10
std::cout << "Timer expired!" << std::endl;
11
folly::EventBase::current()->terminateLoopSoon(); // 请求退出事件循环
12
};
13
14
// 注册定时器事件,延迟 2 秒后执行回调函数
15
folly::TimeoutManager::addTimeout(std::chrono::seconds(2), timerCallback);
16
17
std::cout << "Starting event loop..." << std::endl;
18
evb.loopForever(); // 启动事件循环
19
20
std::cout << "Event loop finished." << std::endl;
21
return 0;
22
}
这个示例程序创建了一个 EventBase
对象,并使用 folly::TimeoutManager::addTimeout()
注册了一个定时器事件。TimeoutManager
实际上是 EventBase
的一个辅助工具,它内部使用 EventBase
的定时器功能。当程序运行后,会先打印 "Starting event loop...",然后等待 2 秒,定时器到期后,回调函数 timerCallback
会被执行,打印 "Timer expired!",并请求事件循环退出。最后,程序打印 "Event loop finished." 并结束。
通过 EventBase
的事件循环机制,开发者可以构建高效、非阻塞的异步程序,充分利用系统资源,提高程序的性能和响应速度。
5.1.2 使用 EventBase 监听文件描述符事件 (Listening for File Descriptor Events with EventBase)
讲解如何使用 EventBase
监听文件描述符的读写事件,实现非阻塞 I/O 操作。
EventBase
的核心功能之一是监听文件描述符 (file descriptor, fd) 上的事件,例如可读事件 (read event) 和可写事件 (write event)。通过监听这些事件,程序可以在文件描述符准备好进行 I/O 操作时才执行相应的操作,从而实现非阻塞 I/O (non-blocking I/O)。这对于构建高性能网络应用至关重要,因为它允许单个线程处理多个并发连接,避免线程阻塞在 I/O 操作上。
① 文件描述符与事件类型
在 Unix-like 系统中,文件描述符是一个整数,代表着一个打开的文件、套接字或其他 I/O 资源。EventBase
可以监听以下类型的文件描述符事件:
▮▮▮▮ⓐ 可读事件 (Read Event):当文件描述符上有数据可读时(例如,套接字接收到数据),EventBase
会触发可读事件。
▮▮▮▮ⓑ 可写事件 (Write Event):当文件描述符可以写入数据时(例如,套接字发送缓冲区有空间),EventBase
会触发可写事件。
▮▮▮▮ⓒ 错误事件 (Error Event):当文件描述符发生错误时(例如,连接断开),EventBase
会触发错误事件。
▮▮▮▮ⓓ 关闭事件 (Close Event):当文件描述符被关闭时,EventBase
会触发关闭事件。
② 注册文件描述符事件处理器
要使用 EventBase
监听文件描述符事件,需要使用 EventBase::addHandler()
方法注册一个 文件描述符事件处理器 (file descriptor event handler)。addHandler()
方法的原型如下:
1
void addHandler(
2
EventHandler* handler, // 事件处理器对象
3
int fd, // 文件描述符
4
EventBase::PollEvents events // 监听的事件类型
5
);
参数说明:
⚝ handler
:指向 folly::EventHandler
对象的指针。EventHandler
是一个抽象基类,需要自定义类继承它,并实现相应的事件处理方法。
⚝ fd
:要监听的文件描述符。
⚝ events
:一个枚举值,指定要监听的事件类型。可以使用 EventBase::READ
, EventBase::WRITE
, EventBase::READ | EventBase::WRITE
等组合。
folly::EventHandler
抽象基类定义了以下纯虚函数,需要在派生类中实现:
⚝ virtual void readReady() noexcept;
:当文件描述符可读时调用。
⚝ virtual void writeReady() noexcept;
:当文件描述符可写时调用。
⚝ virtual void errorOccurred() noexcept;
:当文件描述符发生错误时调用。
⚝ virtual void connectionLost() noexcept;
:当文件描述符连接丢失时调用(例如,套接字连接断开)。
⚝ virtual void handlerDestroyed() noexcept;
:当事件处理器被销毁时调用,用于清理资源。
③ 自定义文件描述符事件处理器
需要创建一个类继承自 folly::EventHandler
,并重写需要处理的事件方法。例如,创建一个简单的套接字读事件处理器:
1
#include <folly/io/async/EventHandler.h>
2
#include <folly/io/async/EventBase.h>
3
#include <unistd.h> // for read
4
#include <iostream>
5
6
class SocketReadHandler : public folly::EventHandler {
7
public:
8
SocketReadHandler(folly::EventBase* evb, int fd) :
9
folly::EventHandler(evb),
10
fd_(fd) {}
11
12
~SocketReadHandler() override {
13
close(fd_); // 关闭文件描述符
14
std::cout << "SocketReadHandler destroyed, fd closed." << std::endl;
15
}
16
17
void readReady() noexcept override {
18
char buffer[1024];
19
ssize_t bytesRead = read(fd_, buffer, sizeof(buffer));
20
if (bytesRead > 0) {
21
std::cout << "Received data: " << std::string(buffer, bytesRead) << std::endl;
22
} else if (bytesRead == 0) {
23
std::cout << "Connection closed by peer." << std::endl;
24
getEventBase()->removeHandler(this); // 移除事件处理器
25
delete this; // 销毁事件处理器对象
26
} else {
27
perror("read error");
28
getEventBase()->removeHandler(this); // 移除事件处理器
29
delete this; // 销毁事件处理器对象
30
}
31
}
32
33
private:
34
int fd_;
35
};
在这个例子中,SocketReadHandler
继承自 folly::EventHandler
,并在 readReady()
方法中实现了读取套接字数据的逻辑。当套接字可读时,readReady()
会被 EventBase
调用,读取数据并打印到控制台。如果读取到 0 字节,表示连接已关闭,此时需要移除事件处理器并销毁对象。如果 read()
调用出错,也需要处理错误并移除事件处理器。
④ 示例:监听套接字读事件
以下示例演示如何创建一个 TCP 服务器,并使用 EventBase
监听客户端连接的套接字读事件。
1
#include <folly/io/async/EventBase.h>
2
#include <folly/io/async/AsyncServerSocket.h>
3
#include <folly/SocketAddress.h>
4
#include <iostream>
5
6
class AcceptHandler : public folly::AsyncServerSocket::AcceptCallback {
7
public:
8
AcceptHandler(folly::EventBase* evb) : evb_(evb) {}
9
10
void connectionAccepted(int socketFd, const folly::SocketAddress& /* clientAddress */) noexcept override {
11
std::cout << "Connection accepted from client, fd = " << socketFd << std::endl;
12
// 创建 SocketReadHandler 并注册到 EventBase
13
auto readHandler = new SocketReadHandler(evb_, socketFd);
14
evb_->addHandler(readHandler, socketFd, folly::EventBase::READ);
15
}
16
17
void listenError(const std::exception& ex) noexcept override {
18
std::cerr << "Listen error: " << ex.what() << std::endl;
19
evb_->terminateLoopSoon();
20
}
21
22
private:
23
folly::EventBase* evb_;
24
};
25
26
27
int main() {
28
folly::EventBase evb;
29
folly::AsyncServerSocket serverSocket(&evb);
30
folly::SocketAddress listenAddress("0.0.0.0", 8080);
31
32
AcceptHandler acceptHandler(&evb);
33
34
try {
35
serverSocket.bind(listenAddress);
36
serverSocket.listen(10); // 监听队列长度为 10
37
serverSocket.setAcceptCallback(&acceptHandler);
38
std::cout << "Server listening on port 8080..." << std::endl;
39
evb.loopForever(); // 启动事件循环
40
} catch (const std::exception& e) {
41
std::cerr << "Exception: " << e.what() << std::endl;
42
return 1;
43
}
44
45
std::cout << "Server stopped." << std::endl;
46
return 0;
47
}
这个示例程序创建了一个 AsyncServerSocket
,它内部使用 EventBase
来处理监听套接字上的事件。AcceptHandler
实现了 folly::AsyncServerSocket::AcceptCallback
接口,用于处理新的客户端连接。当有新的连接到达时,connectionAccepted()
方法会被调用,在这个方法中,创建了 SocketReadHandler
对象,并将其注册到 EventBase
,监听新连接套接字上的读事件。这样,当客户端发送数据时,SocketReadHandler::readReady()
方法就会被调用,处理接收到的数据。
通过这种方式,可以使用 EventBase
高效地监听和处理文件描述符事件,构建高性能的异步网络应用。
5.1.3 EventBase 的定时器与延迟任务 (Timers and Delayed Tasks in EventBase)
介绍如何使用 EventBase
的定时器功能,实现定时任务和延迟操作。
EventBase
提供了内置的定时器 (timer) 功能,允许程序注册定时任务,在指定的时间点或延迟一段时间后执行特定的操作。定时器在很多场景下都非常有用,例如:
⚝ 周期性任务 (Periodic Tasks):例如,定期发送心跳包、轮询服务器状态等。
⚝ 延迟执行 (Delayed Execution):例如,延迟一段时间后执行某个操作,如重试机制、延迟加载等。
⚝ 超时处理 (Timeout Handling):例如,设置网络请求的超时时间,超过时间后取消请求。
EventBase
的定时器功能主要通过 TimeoutManager
类来提供。TimeoutManager
是 EventBase
的一个辅助类,用于管理和调度定时器事件。
① 注册定时器事件
可以使用 folly::TimeoutManager::addTimeout()
方法注册定时器事件。该方法有多个重载版本,常用的原型如下:
1
static Timeout* addTimeout(
2
Duration duration, // 延迟时间
3
FuncType func // 定时器回调函数
4
);
5
6
static Timeout* addTimeout(
7
TimePoint timePoint, // 目标时间点
8
FuncType func // 定时器回调函数
9
);
10
11
static Timeout* addTimeout(
12
Duration duration,
13
FuncType func,
14
DestructorType destructor // 可选的析构函数
15
);
参数说明:
⚝ duration
:延迟时间,类型为 std::chrono::Duration
,例如 std::chrono::seconds(2)
表示 2 秒。
⚝ timePoint
:目标时间点,类型为 std::chrono::TimePoint
,例如 std::chrono::system_clock::now() + std::chrono::seconds(5)
表示 5 秒后的时间点。
⚝ func
:定时器回调函数,类型为 FuncType
,通常是一个无参数的 std::function<void()>
或 lambda 表达式。
⚝ destructor
:可选的析构函数,类型为 DestructorType
,用于在定时器被取消或执行后清理资源。
addTimeout()
方法返回一个 folly::Timeout*
指针,可以用于取消定时器。
② 定时器回调函数
定时器回调函数 func
在定时器到期时被 EventBase
调用。回调函数通常执行定时任务的逻辑。在回调函数中,可以使用 folly::EventBase::current()
获取当前线程的 EventBase
对象,并进行其他操作,例如再次注册定时器以实现周期性任务,或请求退出事件循环。
③ 取消定时器
可以使用 folly::Timeout::cancel()
方法取消已注册的定时器。cancel()
方法会阻止定时器回调函数被执行。取消定时器通常在不再需要定时任务时进行,例如在程序退出或任务完成时。
1
folly::Timeout* timeout = folly::TimeoutManager::addTimeout(
2
std::chrono::seconds(5),
3
[]() {
4
std::cout << "This timer will be executed after 5 seconds." << std::endl;
5
}
6
);
7
8
// ... 在某个条件下取消定时器
9
timeout->cancel();
④ 示例:周期性定时器
以下示例演示如何使用 EventBase
实现一个周期性定时器,每隔 1 秒打印当前时间。
1
#include <folly/io/async/EventBase.h>
2
#include <folly/io/async/TimeoutManager.h>
3
#include <iostream>
4
#include <chrono>
5
#include <iomanip> // for std::put_time
6
#include <ctime> // for std::localtime, std::time
7
8
folly::Timeout* periodicTimer; // 用于存储定时器指针
9
10
void periodicTask() {
11
auto now = std::chrono::system_clock::now();
12
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
13
std::tm now_tm;
14
localtime_r(&now_c, &now_tm); // thread-safe version of localtime
15
16
std::cout << "Current time: " << std::put_time(&now_tm, "%Y-%m-%d %H:%M:%S") << std::endl;
17
18
// 重新注册定时器,实现周期性任务
19
periodicTimer = folly::TimeoutManager::addTimeout(
20
std::chrono::seconds(1),
21
periodicTask
22
);
23
}
24
25
int main() {
26
folly::EventBase evb;
27
28
// 启动周期性定时器
29
periodicTimer = folly::TimeoutManager::addTimeout(
30
std::chrono::seconds(1),
31
periodicTask
32
);
33
34
std::cout << "Starting periodic timer, press Ctrl+C to stop..." << std::endl;
35
evb.loopForever(); // 启动事件循环
36
37
std::cout << "Event loop finished." << std::endl;
38
return 0;
39
}
这个示例程序定义了一个 periodicTask()
函数,用于打印当前时间,并在函数末尾重新注册一个 1 秒后的定时器,从而实现周期性调用。在 main()
函数中,首先启动了周期性定时器,然后启动了 EventBase
的事件循环。程序会每隔 1 秒打印一次当前时间,直到用户按下 Ctrl+C 终止程序。
⑤ 示例:延迟任务
以下示例演示如何使用 EventBase
实现一个延迟任务,延迟 3 秒后打印一条消息。
1
#include <folly/io/async/EventBase.h>
2
#include <folly/io/async/TimeoutManager.h>
3
#include <iostream>
4
5
int main() {
6
folly::EventBase evb;
7
8
// 定义延迟任务回调函数
9
auto delayedTask = []() {
10
std::cout << "Delayed task executed after 3 seconds." << std::endl;
11
folly::EventBase::current()->terminateLoopSoon(); // 请求退出事件循环
12
};
13
14
// 注册延迟任务,延迟 3 秒后执行回调函数
15
folly::TimeoutManager::addTimeout(std::chrono::seconds(3), delayedTask);
16
17
std::cout << "Starting event loop with delayed task..." << std::endl;
18
evb.loopForever(); // 启动事件循环
19
20
std::cout << "Event loop finished." << std::endl;
21
return 0;
22
}
这个示例程序与 5.1.1 节的示例类似,注册了一个延迟 3 秒的定时器事件,并在定时器到期后执行回调函数 delayedTask
,打印一条消息并退出事件循环。
通过 EventBase
的定时器功能,开发者可以方便地实现各种定时任务和延迟操作,构建更加灵活和功能丰富的异步应用。
5.2 AsyncSocket:异步套接字编程 (AsyncSocket: Asynchronous Socket Programming)
介绍 AsyncSocket
的 API 和用法,讲解如何使用 AsyncSocket
进行异步套接字编程,实现非阻塞的网络通信。
AsyncSocket
是 Folly 库中用于进行异步套接字编程 (asynchronous socket programming) 的核心组件。它基于 EventBase
的事件循环机制,提供了非阻塞的套接字 I/O 操作,使得程序可以在等待网络数据传输时继续执行其他任务,从而提高网络应用的并发性和性能。AsyncSocket
封装了底层的套接字操作,并提供了易于使用的 API,简化了异步网络编程的复杂性。
5.2.1 AsyncSocket 的创建与连接 (Creating and Connecting AsyncSockets)
讲解如何创建 AsyncSocket
对象,以及如何连接到远程服务器或监听端口。
① 创建 AsyncSocket 对象
要使用 AsyncSocket
进行网络编程,首先需要创建 folly::AsyncSocket
对象。AsyncSocket
的构造函数需要一个 folly::EventBase*
参数,用于指定 AsyncSocket
对象所属的事件循环。
1
#include <folly/io/async/AsyncSocket.h>
2
#include <folly/io/async/EventBase.h>
3
4
folly::EventBase evb;
5
folly::AsyncSocket socket(&evb);
这段代码创建了一个 EventBase
对象 evb
,然后创建了一个 AsyncSocket
对象 socket
,并将其关联到 evb
事件循环。
② 连接到远程服务器 (客户端)
作为客户端,AsyncSocket
可以连接到远程服务器。连接操作是异步的,需要指定连接成功和连接失败的回调函数。可以使用 AsyncSocket::connect()
方法发起连接。
1
#include <folly/SocketAddress.h>
2
#include <folly/io/async/AsyncSocket.h>
3
#include <folly/io/async/EventBase.h>
4
#include <iostream>
5
6
class ConnectCallback : public folly::AsyncSocket::ConnectCallback {
7
public:
8
void connectSuccess() noexcept override {
9
std::cout << "Connected to server successfully." << std::endl;
10
// 连接成功后的操作,例如发送数据
11
}
12
13
void connectError(const folly::AsyncSocketException& ex) noexcept override {
14
std::cerr << "Failed to connect to server: " << ex.what() << std::endl;
15
// 连接失败后的处理,例如重试或退出
16
folly::EventBase::current()->terminateLoopSoon(); // 请求退出事件循环
17
}
18
};
19
20
int main() {
21
folly::EventBase evb;
22
folly::AsyncSocket socket(&evb);
23
folly::SocketAddress serverAddress("127.0.0.1", 8080); // 服务器地址和端口
24
ConnectCallback connectCallback;
25
26
socket.connect(&connectCallback, serverAddress); // 发起连接
27
28
std::cout << "Connecting to server..." << std::endl;
29
evb.loopForever(); // 启动事件循环
30
31
std::cout << "Event loop finished." << std::endl;
32
return 0;
33
}
在这个例子中,创建了一个 ConnectCallback
类,继承自 folly::AsyncSocket::ConnectCallback
,并实现了 connectSuccess()
和 connectError()
方法,分别处理连接成功和连接失败的情况。AsyncSocket::connect()
方法接受一个 ConnectCallback
对象和一个 folly::SocketAddress
对象作为参数,发起异步连接。连接结果会通过回调函数通知。
③ 监听端口 (服务器)
作为服务器,AsyncSocket
可以监听指定的端口,等待客户端连接。监听操作通常使用 folly::AsyncServerSocket
类,它内部使用了 AsyncSocket
和 EventBase
。在 5.1.2 节的示例中已经展示了 AsyncServerSocket
的基本用法。
1
#include <folly/io/async/AsyncServerSocket.h>
2
#include <folly/SocketAddress.h>
3
#include <folly/io/async/EventBase.h>
4
#include <iostream>
5
6
class AcceptCallback : public folly::AsyncServerSocket::AcceptCallback {
7
public:
8
void connectionAccepted(int socketFd, const folly::SocketAddress& clientAddress) noexcept override {
9
std::cout << "Connection accepted from " << clientAddress.getAddressStr() << ":" << clientAddress.getPort()
10
<< ", fd = " << socketFd << std::endl;
11
// 处理新的连接,例如创建 AsyncSocket 或注册事件处理器
12
// ...
13
}
14
15
void listenError(const std::exception& ex) noexcept override {
16
std::cerr << "Listen error: " << ex.what() << std::endl;
17
folly::EventBase::current()->terminateLoopSoon();
18
}
19
};
20
21
int main() {
22
folly::EventBase evb;
23
folly::AsyncServerSocket serverSocket(&evb);
24
folly::SocketAddress listenAddress("0.0.0.0", 8080); // 监听地址和端口
25
AcceptCallback acceptCallback;
26
27
try {
28
serverSocket.bind(listenAddress);
29
serverSocket.listen(10); // 监听队列长度
30
serverSocket.setAcceptCallback(&acceptCallback); // 设置接受连接回调
31
std::cout << "Server listening on port 8080..." << std::endl;
32
evb.loopForever(); // 启动事件循环
33
} catch (const std::exception& e) {
34
std::cerr << "Exception: " << e.what() << std::endl;
35
return 1;
36
}
37
38
std::cout << "Server stopped." << std::endl;
39
return 0;
40
}
AsyncServerSocket
的使用步骤包括:
▮▮▮▮ⓐ 创建 folly::AsyncServerSocket
对象,并关联到 EventBase
。
▮▮▮▮ⓑ 创建一个类实现 folly::AsyncServerSocket::AcceptCallback
接口,并实现 connectionAccepted()
和 listenError()
方法。
▮▮▮▮ⓒ 调用 AsyncServerSocket::bind()
方法绑定监听地址和端口。
▮▮▮▮ⓓ 调用 AsyncServerSocket::listen()
方法开始监听,并设置监听队列长度。
▮▮▮▮ⓔ 调用 AsyncServerSocket::setAcceptCallback()
方法设置接受连接回调对象。
▮▮▮▮ⓕ 启动 EventBase
的事件循环。
当有新的客户端连接到达时,AcceptCallback::connectionAccepted()
方法会被调用,可以在这个方法中处理新的连接,例如创建新的 AsyncSocket
对象或注册事件处理器来处理客户端的请求。
通过 AsyncSocket
和 AsyncServerSocket
,可以方便地创建客户端和服务器端程序,实现异步的网络连接和通信。
5.2.2 AsyncSocket 的异步读写操作 (Asynchronous Read and Write Operations of AsyncSocket)
介绍 AsyncSocket
的异步读写接口,如 read
, write
, recv
, send
等,以及回调函数的用法。
AsyncSocket
提供了多种异步读写操作的接口,使得程序可以在不阻塞线程的情况下进行数据收发。这些接口通常使用回调函数 (callback function) 来通知操作完成或发生错误。
① 异步读取数据 (Asynchronous Read)
AsyncSocket
提供了多种异步读取数据的方法,常用的有:
▮▮▮▮ⓐ read(ReadCallback* callback, size_t count)
:尝试读取指定字节数的数据。
▮▮▮▮ⓑ readline(ReadCallback* callback, size_t maxLineLength)
:读取一行数据,以换行符 \n
或 \r\n
结尾。
▮▮▮▮ⓒ recv(ReadCallback* callback, size_t count)
:类似于 read()
,但用于 UDP 套接字,可以接收来自任意地址的数据。
▮▮▮▮ⓓ recvmsg(ReadCallback* callback, size_t count)
:类似于 recv()
,但可以接收更多控制信息,如源地址、接口等。
这些读取方法都需要一个 ReadCallback
对象作为参数,用于接收读取结果的通知。folly::AsyncSocket::ReadCallback
是一个抽象基类,需要自定义类继承它,并实现以下方法:
⚝ virtual void readSuccess(folly::AsyncSocket* sock, size_t bytesRead) noexcept;
:读取数据成功时调用,bytesRead
参数表示实际读取的字节数。
⚝ virtual void readError(folly::AsyncSocket* sock, const folly::AsyncSocketException& ex) noexcept;
:读取数据失败时调用,ex
参数包含错误信息。
⚝ virtual void readBufferAvailable(folly::AsyncSocket* sock) noexcept;
:当有数据可读时调用,可以用于流式读取数据。
⚝ virtual void readEOF(folly::AsyncSocket* sock) noexcept;
:当连接关闭时调用,表示对端发送了 EOF。
⚝ virtual void connectionLost(folly::AsyncSocket* sock) noexcept;
:当连接丢失时调用。
例如,使用 read()
方法异步读取数据的示例:
1
#include <folly/io/async/AsyncSocket.h>
2
#include <folly/io/async/EventBase.h>
3
#include <folly/SocketAddress.h>
4
#include <iostream>
5
6
class MyReadCallback : public folly::AsyncSocket::ReadCallback {
7
public:
8
MyReadCallback(folly::AsyncSocket* sock) : sock_(sock) {}
9
10
void readSuccess(folly::AsyncSocket* sock, size_t bytesRead) noexcept override {
11
std::cout << "Read " << bytesRead << " bytes." << std::endl;
12
// 处理读取到的数据,例如打印到控制台
13
std::cout << "Data: " << std::string(readBuffer_.data(), bytesRead) << std::endl;
14
readBuffer_.clear(); // 清空缓冲区
15
16
// 再次发起读取请求,接收更多数据
17
sock_->read(this, 1024); // 继续读取 1024 字节
18
}
19
20
void readError(folly::AsyncSocket* sock, const folly::AsyncSocketException& ex) noexcept override {
21
std::cerr << "Read error: " << ex.what() << std::endl;
22
folly::EventBase::current()->terminateLoopSoon(); // 请求退出事件循环
23
}
24
25
void getReadBuffer(folly::IOBufQueue& buffer) noexcept override {
26
buffer.append(readBuffer_); // 提供读取缓冲区
27
}
28
29
private:
30
folly::AsyncSocket* sock_;
31
folly::IOBufQueue readBuffer_; // 读取缓冲区
32
};
33
34
class ConnectCallback : public folly::AsyncSocket::ConnectCallback {
35
public:
36
ConnectCallback(folly::AsyncSocket* sock) : sock_(sock) {}
37
38
void connectSuccess() noexcept override {
39
std::cout << "Connected to server successfully, start reading..." << std::endl;
40
readCallback_ = new MyReadCallback(sock_); // 创建 ReadCallback 对象
41
sock_->read(readCallback_, 1024); // 发起第一次读取请求,读取 1024 字节
42
}
43
44
void connectError(const folly::AsyncSocketException& ex) noexcept override {
45
std::cerr << "Connect error: " << ex.what() << std::endl;
46
folly::EventBase::current()->terminateLoopSoon();
47
}
48
49
private:
50
folly::AsyncSocket* sock_;
51
MyReadCallback* readCallback_;
52
};
53
54
55
int main() {
56
folly::EventBase evb;
57
folly::AsyncSocket socket(&evb);
58
folly::SocketAddress serverAddress("127.0.0.1", 8080);
59
ConnectCallback connectCallback(&socket);
60
61
socket.connect(&connectCallback, serverAddress);
62
63
std::cout << "Connecting to server and waiting for data..." << std::endl;
64
evb.loopForever();
65
66
std::cout << "Event loop finished." << std::endl;
67
return 0;
68
}
在这个例子中,MyReadCallback
实现了 ReadCallback
接口,在 readSuccess()
方法中处理读取成功的数据,并再次发起读取请求,实现持续读取数据。ConnectCallback
在连接成功后,创建 MyReadCallback
对象,并发起第一次读取请求。
② 异步写入数据 (Asynchronous Write)
AsyncSocket
也提供了异步写入数据的方法:
▮▮▮▮ⓐ write(WriteCallback* callback, std::unique_ptr<folly::IOBuf> iobuf)
:将 folly::IOBuf
中的数据写入套接字。
▮▮▮▮ⓑ writev(WriteCallback* callback, const iovec* vec, size_t count)
:使用 iovec
数组写入数据,可以写入多个不连续的内存块。
▮▮▮▮ⓒ send(WriteCallback* callback, std::unique_ptr<folly::IOBuf> iobuf, const folly::SocketAddress& address)
:类似于 write()
,但用于 UDP 套接字,可以发送数据到指定地址。
▮▮▮▮ⓓ sendmsg(WriteCallback* callback, std::unique_ptr<folly::IOBuf> iobuf, const folly::SocketAddress& address, int flags, const folly::ControlData& controlData)
:类似于 send()
,但可以发送更多控制信息。
这些写入方法都需要一个 WriteCallback
对象作为参数,用于接收写入结果的通知。folly::AsyncSocket::WriteCallback
是一个抽象基类,需要自定义类继承它,并实现以下方法:
⚝ virtual void writeSuccess(folly::AsyncSocket* sock) noexcept;
:写入数据成功时调用。
⚝ virtual void writeError(folly::AsyncSocket* sock, const folly::AsyncSocketException& ex) noexcept;
:写入数据失败时调用,ex
参数包含错误信息。
⚝ virtual void writeBufferFull(folly::AsyncSocket* sock) noexcept;
:当写入缓冲区满时调用,可以暂停写入操作。
⚝ virtual void writeBufferEmpty(folly::AsyncSocket* sock) noexcept;
:当写入缓冲区变空时调用,可以继续写入操作。
⚝ virtual void connectionLost(folly::AsyncSocket* sock) noexcept;
:当连接丢失时调用。
例如,使用 write()
方法异步写入数据的示例:
1
#include <folly/io/async/AsyncSocket.h>
2
#include <folly/io/async/EventBase.h>
3
#include <folly/SocketAddress.h>
4
#include <folly/io/IOBuf.h>
5
#include <iostream>
6
7
class MyWriteCallback : public folly::AsyncSocket::WriteCallback {
8
public:
9
void writeSuccess(folly::AsyncSocket* sock) noexcept override {
10
std::cout << "Write success." << std::endl;
11
folly::EventBase::current()->terminateLoopSoon(); // 写入成功后退出事件循环
12
}
13
14
void writeError(folly::AsyncSocket* sock, const folly::AsyncSocketException& ex) noexcept override {
15
std::cerr << "Write error: " << ex.what() << std::endl;
16
folly::EventBase::current()->terminateLoopSoon(); // 写入失败后退出事件循环
17
}
18
};
19
20
class ConnectCallback : public folly::AsyncSocket::ConnectCallback {
21
public:
22
ConnectCallback(folly::AsyncSocket* sock) : sock_(sock) {}
23
24
void connectSuccess() noexcept override {
25
std::cout << "Connected to server successfully, start writing..." << std::endl;
26
writeCallback_ = new MyWriteCallback(); // 创建 WriteCallback 对象
27
auto iobuf = folly::IOBuf::copyBuffer("Hello, server!"); // 创建 IOBuf
28
sock_->write(writeCallback_, std::move(iobuf)); // 发起写入请求
29
}
30
31
void connectError(const folly::AsyncSocketException& ex) noexcept override {
32
std::cerr << "Connect error: " << ex.what() << std::endl;
33
folly::EventBase::current()->terminateLoopSoon();
34
}
35
36
private:
37
folly::AsyncSocket* sock_;
38
MyWriteCallback* writeCallback_;
39
};
40
41
42
int main() {
43
folly::EventBase evb;
44
folly::AsyncSocket socket(&evb);
45
folly::SocketAddress serverAddress("127.0.0.1", 8080);
46
ConnectCallback connectCallback(&socket);
47
48
socket.connect(&connectCallback, serverAddress);
49
50
std::cout << "Connecting to server and sending data..." << std::endl;
51
evb.loopForever();
52
53
std::cout << "Event loop finished." << std::endl;
54
return 0;
55
}
在这个例子中,MyWriteCallback
实现了 WriteCallback
接口,在 writeSuccess()
方法中处理写入成功的情况。ConnectCallback
在连接成功后,创建 MyWriteCallback
对象,并创建一个包含 "Hello, server!" 字符串的 folly::IOBuf
对象,然后发起写入请求。
通过 AsyncSocket
的异步读写接口和回调函数机制,可以实现高效、非阻塞的网络通信。
5.2.3 AsyncSocket 的错误处理与连接管理 (Error Handling and Connection Management in AsyncSocket)
讲解如何在 AsyncSocket
编程中处理网络错误、连接断开和超时等问题。
在异步网络编程中,错误处理和连接管理是非常重要的方面。AsyncSocket
提供了一些机制来处理常见的网络问题,例如连接错误、读写错误、连接断开和超时。
① 错误处理 (Error Handling)
AsyncSocket
的异步操作(如 connect
, read
, write
等)通过回调函数来通知操作结果,包括成功和失败。当操作失败时,会调用相应的错误回调函数,例如 ConnectCallback::connectError()
, ReadCallback::readError()
, WriteCallback::writeError()
等。这些错误回调函数会接收一个 folly::AsyncSocketException
对象,其中包含了详细的错误信息。
在错误回调函数中,应该根据具体的错误类型和业务逻辑进行处理。常见的错误处理操作包括:
▮▮▮▮ⓐ 打印错误日志 (Logging Error):记录错误信息,方便调试和排查问题。
▮▮▮▮ⓑ 重试操作 (Retrying Operation):对于某些 transient errors (瞬时错误),例如网络拥塞,可以尝试重新发起操作。
▮▮▮▮ⓒ 关闭连接 (Closing Connection):对于 fatal errors (致命错误),例如连接断开,通常需要关闭当前的 AsyncSocket
连接。
▮▮▮▮ⓓ 通知上层应用 (Notifying Application):将错误信息传递给上层应用,让应用层决定如何处理。
例如,在 ReadCallback::readError()
中处理读取错误的示例:
1
class MyReadCallback : public folly::AsyncSocket::ReadCallback {
2
public:
3
// ...
4
5
void readError(folly::AsyncSocket* sock, const folly::AsyncSocketException& ex) noexcept override {
6
std::cerr << "Read error: " << ex.what() << std::endl;
7
if (ex.getType() == folly::AsyncSocketException::AsyncSocketExceptionType::NETWORK_ERROR) {
8
// 网络错误,可能是连接断开
9
std::cout << "Network error, connection might be lost." << std::endl;
10
sock->close(); // 关闭连接
11
} else {
12
// 其他类型的错误
13
std::cout << "Other read error." << std::endl;
14
}
15
folly::EventBase::current()->terminateLoopSoon(); // 退出事件循环
16
}
17
18
// ...
19
};
② 连接断开处理 (Connection Lost Handling)
当连接意外断开时,AsyncSocket
会调用 ReadCallback::connectionLost()
和 WriteCallback::connectionLost()
回调函数。在这些回调函数中,应该进行连接断开后的清理工作,例如释放资源、通知上层应用等。
1
class MyReadCallback : public folly::AsyncSocket::ReadCallback {
2
public:
3
// ...
4
5
void connectionLost(folly::AsyncSocket* sock) noexcept override {
6
std::cout << "Connection lost." << std::endl;
7
sock->close(); // 关闭连接(如果尚未关闭)
8
folly::EventBase::current()->terminateLoopSoon(); // 退出事件循环
9
}
10
11
// ...
12
};
13
14
class MyWriteCallback : public folly::AsyncSocket::WriteCallback {
15
public:
16
// ...
17
18
void connectionLost(folly::AsyncSocket* sock) noexcept override {
19
std::cout << "Connection lost during write." << std::endl;
20
sock->close(); // 关闭连接(如果尚未关闭)
21
folly::EventBase::current()->terminateLoopSoon(); // 退出事件循环
22
}
23
24
// ...
25
};
③ 超时处理 (Timeout Handling)
AsyncSocket
提供了设置读写超时时间的功能,可以使用 AsyncSocket::setReadTimeout()
和 AsyncSocket::setWriteTimeout()
方法设置超时时间。当读或写操作超过指定的超时时间仍未完成时,AsyncSocket
会触发超时错误,并调用相应的错误回调函数。
1
folly::AsyncSocket socket(&evb);
2
socket.setReadTimeout(std::chrono::seconds(10)); // 设置读超时时间为 10 秒
3
socket.setWriteTimeout(std::chrono::seconds(5)); // 设置写超时时间为 5 秒
在设置超时时间后,如果在超时时间内没有完成读写操作,会触发 folly::AsyncSocketException
异常,异常类型为 TIMEOUT
。可以在错误回调函数中检查异常类型,判断是否是超时错误。
1
class MyReadCallback : public folly::AsyncSocket::ReadCallback {
2
public:
3
// ...
4
5
void readError(folly::AsyncSocket* sock, const folly::AsyncSocketException& ex) noexcept override {
6
std::cerr << "Read error: " << ex.what() << std::endl;
7
if (ex.getType() == folly::AsyncSocketException::AsyncSocketExceptionType::TIMEOUT) {
8
std::cout << "Read timeout." << std::endl;
9
sock->close(); // 超时后关闭连接
10
} else {
11
// 其他类型的错误处理
12
}
13
folly::EventBase::current()->terminateLoopSoon();
14
}
15
16
// ...
17
};
④ 连接关闭 (Connection Closing)
可以使用 AsyncSocket::close()
方法手动关闭连接。关闭连接会立即停止所有的读写操作,并释放相关的资源。在关闭连接后,AsyncSocket
会调用 ReadCallback::readEOF()
和 WriteCallback::connectionLost()
回调函数,通知连接已关闭。
1
folly::AsyncSocket socket(&evb);
2
// ...
3
socket.close(); // 手动关闭连接
在网络编程中,合理的错误处理和连接管理策略对于保证程序的健壮性和可靠性至关重要。AsyncSocket
提供的错误回调、连接断开通知和超时机制,可以帮助开发者更好地处理各种网络异常情况,构建稳定可靠的异步网络应用。
5.3 简易 HTTP 服务器与客户端 (Simple HTTP Server and Client)
介绍如何使用 Folly 库的网络组件构建简易的 HTTP 服务器和客户端,演示 Folly 在实际网络应用中的应用。
本节将演示如何使用 Folly 库的 AsyncSocket
和 EventBase
组件构建一个简单的 HTTP 服务器和客户端。虽然这里实现的 HTTP 服务器和客户端功能非常基础,但可以帮助读者理解如何将 Folly 的网络编程组件应用于实际的网络应用开发中。
5.3.1 基于 AsyncSocket 构建 HTTP 服务器 (Building an HTTP Server based on AsyncSocket)
演示如何使用 AsyncSocket
和 EventBase
构建一个基本的 HTTP 服务器,处理 HTTP 请求和响应。
① HTTP 服务器基本框架
一个基本的 HTTP 服务器需要完成以下几个主要任务:
▮▮▮▮ⓐ 监听端口 (Listening Port):服务器需要监听指定的端口,等待客户端连接。
▮▮▮▮ⓑ 接受连接 (Accepting Connection):当有客户端连接请求到达时,服务器需要接受连接,并创建一个新的套接字用于与客户端通信。
▮▮▮▮ⓒ 接收请求 (Receiving Request):从客户端套接字接收 HTTP 请求数据。
▮▮▮▮ⓓ 解析请求 (Parsing Request):解析接收到的 HTTP 请求,获取请求方法、URL、头部和Body等信息。
▮▮▮▮ⓔ 处理请求 (Handling Request):根据解析后的请求信息,执行相应的业务逻辑,生成 HTTP 响应。
▮▮▮▮ⓕ 发送响应 (Sending Response):将生成的 HTTP 响应数据发送回客户端。
▮▮▮▮ⓖ 关闭连接 (Closing Connection):完成请求响应后,关闭与客户端的连接。
② 简易 HTTP 服务器实现
以下是一个使用 AsyncSocket
和 EventBase
构建的简易 HTTP 服务器的示例代码。这个服务器只能处理简单的 GET 请求,并返回一个固定的 "Hello, World!" 响应。
1
#include <folly/io/async/AsyncServerSocket.h>
2
#include <folly/io/async/AsyncSocket.h>
3
#include <folly/io/async/EventBase.h>
4
#include <folly/SocketAddress.h>
5
#include <folly/io/IOBuf.h>
6
#include <iostream>
7
#include <string>
8
9
class HttpServerConnection : public folly::AsyncSocket::ReadCallback, public folly::AsyncSocket::WriteCallback {
10
public:
11
HttpServerConnection(folly::AsyncSocket* sock) : sock_(sock) {
12
sock_->setReadCallback(this);
13
sock_->setWriteCallback(this);
14
}
15
16
~HttpServerConnection() override {
17
delete sock_;
18
std::cout << "HttpServerConnection destroyed, socket closed." << std::endl;
19
}
20
21
void onDataAvailable() noexcept {
22
sock_->read(this, 4096); // 尝试读取 4KB 数据
23
}
24
25
void readSuccess(folly::AsyncSocket* sock, size_t bytesRead) noexcept override {
26
if (bytesRead == 0) {
27
std::cout << "Client closed connection." << std::endl;
28
delete this; // 客户端关闭连接
29
return;
30
}
31
32
std::string requestData(readBuffer_.data(), bytesRead);
33
std::cout << "Received request:\n" << requestData << std::endl;
34
readBuffer_.clear();
35
36
// 简易 HTTP 响应
37
std::string response =
38
"HTTP/1.1 200 OK\r\n"
39
"Content-Type: text/plain\r\n"
40
"Content-Length: 13\r\n"
41
"Connection: close\r\n"
42
"\r\n"
43
"Hello, World!";
44
45
auto iobuf = folly::IOBuf::copyBuffer(response);
46
sock_->write(this, std::move(iobuf)); // 发送响应
47
}
48
49
void readError(folly::AsyncSocket* sock, const folly::AsyncSocketException& ex) noexcept override {
50
std::cerr << "Read error: " << ex.what() << std::endl;
51
delete this; // 发生错误,关闭连接
52
}
53
54
void getReadBuffer(folly::IOBufQueue& buffer) noexcept override {
55
buffer.append(readBuffer_);
56
}
57
58
void writeSuccess(folly::AsyncSocket* sock) noexcept override {
59
std::cout << "Response sent." << std::endl;
60
sock->close(); // 发送完响应后关闭连接
61
delete this; // 关闭连接后销毁连接对象
62
}
63
64
void writeError(folly::AsyncSocket* sock, const folly::AsyncSocketException& ex) noexcept override {
65
std::cerr << "Write error: " << ex.what() << std::endl;
66
delete this; // 发生错误,关闭连接
67
}
68
69
void connectionLost(folly::AsyncSocket* sock) noexcept override {
70
std::cout << "Connection lost." << std::endl;
71
delete this; // 连接丢失,销毁连接对象
72
}
73
74
private:
75
folly::AsyncSocket* sock_;
76
folly::IOBufQueue readBuffer_;
77
};
78
79
80
class HttpServerAcceptHandler : public folly::AsyncServerSocket::AcceptCallback {
81
public:
82
HttpServerAcceptHandler(folly::EventBase* evb) : evb_(evb) {}
83
84
void connectionAccepted(int socketFd, const folly::SocketAddress& /* clientAddress */) noexcept override {
85
std::cout << "Connection accepted, fd = " << socketFd << std::endl;
86
folly::AsyncSocket* sock = new folly::AsyncSocket(evb_, socketFd);
87
new HttpServerConnection(sock); // 创建连接处理对象
88
}
89
90
void listenError(const std::exception& ex) noexcept override {
91
std::cerr << "Listen error: " << ex.what() << std::endl;
92
folly::EventBase::current()->terminateLoopSoon();
93
}
94
95
private:
96
folly::EventBase* evb_;
97
};
98
99
100
int main() {
101
folly::EventBase evb;
102
folly::AsyncServerSocket serverSocket(&evb);
103
folly::SocketAddress listenAddress("0.0.0.0", 8080);
104
HttpServerAcceptHandler acceptHandler(&evb);
105
106
try {
107
serverSocket.bind(listenAddress);
108
serverSocket.listen(10);
109
serverSocket.setAcceptCallback(&acceptHandler);
110
std::cout << "Simple HTTP server listening on port 8080..." << std::endl;
111
evb.loopForever();
112
} catch (const std::exception& e) {
113
std::cerr << "Exception: " << e.what() << std::endl;
114
return 1;
115
}
116
117
std::cout << "Server stopped." << std::endl;
118
return 0;
119
}
在这个示例中,HttpServerConnection
类处理每个客户端连接的请求和响应。它同时实现了 folly::AsyncSocket::ReadCallback
和 folly::AsyncSocket::WriteCallback
接口,用于处理读写事件。HttpServerAcceptHandler
类负责接受新的客户端连接,并为每个连接创建 HttpServerConnection
对象。
这个简易 HTTP 服务器只处理简单的 GET 请求,并返回固定的 "Hello, World!" 响应,没有实现完整的 HTTP 协议解析和处理逻辑。但是,它展示了使用 AsyncSocket
和 EventBase
构建异步服务器的基本框架。
5.3.2 基于 AsyncSocket 构建 HTTP 客户端 (Building an HTTP Client based on AsyncSocket)
演示如何使用 AsyncSocket
和 EventBase
构建一个基本的 HTTP 客户端,发送 HTTP 请求并接收响应。
① HTTP 客户端基本框架
一个基本的 HTTP 客户端需要完成以下几个主要任务:
▮▮▮▮ⓐ 建立连接 (Establishing Connection):客户端需要连接到指定的服务器地址和端口。
▮▮▮▮ⓑ 发送请求 (Sending Request):构造 HTTP 请求数据,并发送到服务器。
▮▮▮▮ⓒ 接收响应 (Receiving Response):从服务器接收 HTTP 响应数据。
▮▮▮▮ⓓ 解析响应 (Parsing Response):解析接收到的 HTTP 响应,获取状态码、头部和Body等信息.
▮▮▮▮ⓔ 处理响应 (Handling Response):根据解析后的响应信息,执行相应的业务逻辑。
▮▮▮▮ⓕ 关闭连接 (Closing Connection):完成请求响应后,关闭与服务器的连接。
② 简易 HTTP 客户端实现
以下是一个使用 AsyncSocket
和 EventBase
构建的简易 HTTP 客户端的示例代码。这个客户端发送一个简单的 GET 请求到指定的服务器,并打印接收到的响应。
1
#include <folly/io/async/AsyncSocket.h>
2
#include <folly/io/async/EventBase.h>
3
#include <folly/SocketAddress.h>
4
#include <folly/io/IOBuf.h>
5
#include <iostream>
6
#include <string>
7
8
class HttpClientConnection : public folly::AsyncSocket::ReadCallback, public folly::AsyncSocket::WriteCallback, public folly::AsyncSocket::ConnectCallback {
9
public:
10
HttpClientConnection(folly::EventBase* evb, const folly::SocketAddress& serverAddress) : evb_(evb), serverAddress_(serverAddress), sock_(new folly::AsyncSocket(evb_)) {
11
sock_->setReadCallback(this);
12
sock_->setWriteCallback(this);
13
}
14
15
~HttpClientConnection() override {
16
delete sock_;
17
std::cout << "HttpClientConnection destroyed, socket closed." << std::endl;
18
}
19
20
void connect() {
21
sock_->connect(this, serverAddress_); // 发起连接
22
}
23
24
void connectSuccess() noexcept override {
25
std::cout << "Connected to server." << std::endl;
26
sendRequest(); // 连接成功后发送 HTTP 请求
27
}
28
29
void connectError(const folly::AsyncSocketException& ex) noexcept override {
30
std::cerr << "Connect error: " << ex.what() << std::endl;
31
delete this; // 连接失败,销毁连接对象
32
folly::EventBase::current()->terminateLoopSoon();
33
}
34
35
void sendRequest() {
36
std::string request =
37
"GET / HTTP/1.1\r\n"
38
"Host: " + serverAddress_.getAddressStr() + ":" + std::to_string(serverAddress_.getPort()) + "\r\n"
39
"Connection: close\r\n"
40
"\r\n";
41
42
auto iobuf = folly::IOBuf::copyBuffer(request);
43
sock_->write(this, std::move(iobuf)); // 发送 HTTP 请求
44
std::cout << "Request sent:\n" << request << std::endl;
45
}
46
47
48
void onDataAvailable() noexcept {
49
sock_->read(this, 4096); // 尝试读取 4KB 数据
50
}
51
52
void readSuccess(folly::AsyncSocket* sock, size_t bytesRead) noexcept override {
53
if (bytesRead == 0) {
54
std::cout << "Server closed connection." << std::endl;
55
delete this; // 服务器关闭连接
56
return;
57
}
58
59
std::string responseData(readBuffer_.data(), bytesRead);
60
std::cout << "Received response:\n" << responseData << std::endl;
61
readBuffer_.clear();
62
delete this; // 接收完响应后销毁连接对象
63
folly::EventBase::current()->terminateLoopSoon(); // 退出事件循环
64
}
65
66
void readError(folly::AsyncSocket* sock, const folly::AsyncSocketException& ex) noexcept override {
67
std::cerr << "Read error: " << ex.what() << std::endl;
68
delete this; // 发生错误,销毁连接对象
69
folly::EventBase::current()->terminateLoopSoon();
70
}
71
72
void getReadBuffer(folly::IOBufQueue& buffer) noexcept override {
73
buffer.append(readBuffer_);
74
}
75
76
void writeSuccess(folly::AsyncSocket* sock) noexcept override {
77
std::cout << "Request write success, waiting for response..." << std::endl;
78
// 请求发送成功,等待接收响应
79
}
80
81
void writeError(folly::AsyncSocket* sock, const folly::AsyncSocketException& ex) noexcept override {
82
std::cerr << "Write error: " << ex.what() << std::endl;
83
delete this; // 发生错误,销毁连接对象
84
folly::EventBase::current()->terminateLoopSoon();
85
}
86
87
void connectionLost(folly::AsyncSocket* sock) noexcept override {
88
std::cout << "Connection lost." << std::endl;
89
delete this; // 连接丢失,销毁连接对象
90
folly::EventBase::current()->terminateLoopSoon();
91
}
92
93
private:
94
folly::EventBase* evb_;
95
folly::SocketAddress serverAddress_;
96
folly::AsyncSocket* sock_;
97
folly::IOBufQueue readBuffer_;
98
};
99
100
101
int main() {
102
folly::EventBase evb;
103
folly::SocketAddress serverAddress("127.0.0.1", 8080); // 服务器地址和端口
104
105
HttpClientConnection* clientConnection = new HttpClientConnection(&evb, serverAddress);
106
clientConnection->connect(); // 发起连接
107
108
std::cout << "Starting HTTP client, connecting to " << serverAddress.getAddressStr() << ":" << serverAddress.getPort() << "..." << std::endl;
109
evb.loopForever();
110
111
std::cout << "Client finished." << std::endl;
112
return 0;
113
}
在这个示例中,HttpClientConnection
类负责处理与服务器的连接、发送 HTTP 请求和接收 HTTP 响应。它同时实现了 folly::AsyncSocket::ReadCallback
, folly::AsyncSocket::WriteCallback
, 和 folly::AsyncSocket::ConnectCallback
接口。connect()
方法发起连接,connectSuccess()
方法在连接成功后发送 HTTP 请求,readSuccess()
方法处理接收到的 HTTP 响应。
这个简易 HTTP 客户端发送一个简单的 GET 请求,没有实现完整的 HTTP 协议解析和请求构造逻辑。但是,它展示了使用 AsyncSocket
和 EventBase
构建异步客户端的基本流程。
通过这两个简易的 HTTP 服务器和客户端示例,读者可以初步了解如何使用 Folly 库的 AsyncSocket
和 EventBase
组件进行异步网络编程,构建简单的网络应用程序。在实际应用中,可能需要更完善的 HTTP 协议处理、错误处理和连接管理机制,但 Folly 库提供的这些基础组件为构建高性能、可扩展的网络应用提供了强大的支持。
6. 并发与同步:线程、互斥锁与原子操作 (Concurrency and Synchronization: Threads, Mutexes, and Atomic Operations)
本章深入探讨 Folly 库提供的并发与同步工具,包括线程管理、互斥锁、原子操作等,帮助读者构建线程安全、高效的并发程序。
6.1 线程管理与线程池 (Thread Management and Thread Pools)
介绍 Folly 库的线程管理工具,包括线程的创建、销毁和线程池的使用,以及如何高效地管理线程资源。
6.1.1 folly::Thread:线程的封装与管理 (folly::Thread: Thread Encapsulation and Management)
讲解 folly::Thread
的 API 和用法,以及如何使用 folly::Thread
创建和管理线程。
在多线程编程中,直接使用平台提供的线程 API (例如 pthread_create
在 POSIX 系统上,或 CreateThread
在 Windows 上) 虽然可行,但通常较为繁琐且容易出错。folly::Thread
是 Folly 库对线程的一个轻量级封装,旨在简化线程的创建、管理和控制,同时提供更好的跨平台兼容性。folly::Thread
并没有重新发明线程的概念,而是在底层线程 API 之上提供了一层 C++ 接口,使得线程的使用更加安全、方便和高效。
① folly::Thread
的基本用法
创建 folly::Thread
对象非常简单。你可以向其构造函数传递一个函数对象 (function object),该函数对象将会在新线程中执行。例如:
1
#include <folly/Thread.h>
2
#include <iostream>
3
4
void hello_world() {
5
std::cout << "Hello from a Folly thread!" << std::endl;
6
}
7
8
int main() {
9
folly::Thread thread(&hello_world); // 创建并启动线程
10
thread.join(); // 等待线程结束
11
return 0;
12
}
在这个例子中,folly::Thread thread(&hello_world);
创建了一个新的线程,并让它执行 hello_world
函数。thread.join();
会阻塞当前线程,直到新创建的线程执行完成。
② 线程命名
为了方便调试和监控,folly::Thread
允许你为线程命名。你可以通过构造函数的第二个参数来指定线程名称:
1
folly::Thread named_thread(&hello_world, "MyThread");
线程名称在某些调试工具中会显示,有助于区分不同的线程。
③ 获取线程 ID
你可以使用 thread.get আইডি()
方法获取线程的唯一标识符 (ID)。线程 ID 通常是平台相关的,类型为 std::thread::id
。
1
folly::Thread thread(&hello_world);
2
std::thread::id id = thread.getId();
3
std::cout << "Thread ID: " << id << std::endl;
4
thread.join();
④ 分离线程 (Detached Threads)
默认情况下,folly::Thread
对象在其生命周期结束时,必须调用 join()
方法来等待线程完成。如果你希望线程在后台独立运行,而不需要主线程等待,可以调用 detach()
方法。分离后的线程会在后台运行,并在完成时自动释放资源。
1
folly::Thread detached_thread(&hello_world);
2
detached_thread.detach(); // 分离线程,不再需要 join
3
// 主线程继续执行,detached_thread 在后台运行
注意: 一旦线程被分离 (detached),你就不能再调用 join()
方法等待它结束。同时,需要确保分离线程中使用的资源在其生命周期内保持有效,避免悬挂引用 (dangling reference) 等问题。
⑤ 异常处理
如果线程执行的函数抛出异常,folly::Thread
默认会将异常传播到 join()
方法的调用者。这意味着,如果你调用 thread.join()
,并且线程函数抛出了异常,join()
方法也会抛出相同的异常。这有助于及时发现和处理线程中的错误。
1
#include <folly/Thread.h>
2
#include <stdexcept>
3
#include <iostream>
4
5
void throwing_function() {
6
throw std::runtime_error("Something went wrong in the thread!");
7
}
8
9
int main() {
10
folly::Thread thread(&throwing_function);
11
try {
12
thread.join();
13
} catch (const std::exception& e) {
14
std::cerr << "Caught exception from thread: " << e.what() << std::endl;
15
}
16
return 0;
17
}
在这个例子中,throwing_function
抛出了一个 std::runtime_error
异常。当主线程调用 thread.join()
时,这个异常会被捕获并在主线程中处理。
⑥ 总结
folly::Thread
提供了一个简洁、安全且跨平台的线程封装。它简化了线程的创建、命名、ID 获取、分离和异常处理。在需要直接管理单个线程的场景下,folly::Thread
是一个非常有用的工具。然而,在更复杂的并发场景中,例如需要管理大量线程或执行异步任务时,线程池 (ThreadPoolExecutor) 通常是更合适的选择,我们将在下一小节中介绍。
6.1.2 ThreadPoolExecutor:线程池的实现与配置 (ThreadPoolExecutor: Thread Pool Implementation and Configuration)
深入剖析 ThreadPoolExecutor
的实现原理和配置选项,以及如何根据应用场景选择合适的线程池配置。
线程池 (Thread Pool) 是一种高效的线程管理机制,它可以预先创建一组线程,并将待执行的任务放入队列中。线程池中的线程会不断从队列中取出任务并执行。相比于为每个任务都创建和销毁线程,线程池能够显著降低线程创建和销毁的开销,提高系统的响应速度和吞吐量。folly::ThreadPoolExecutor
是 Folly 库提供的功能强大且高度可配置的线程池实现。
① ThreadPoolExecutor
的基本原理
folly::ThreadPoolExecutor
的核心组件包括:
⚝ 工作线程 (Worker Threads): 线程池中预先创建的一组线程,负责执行任务。
⚝ 任务队列 (Task Queue): 用于存储待执行的任务。当有新的任务提交到线程池时,任务会被放入任务队列中。
⚝ 任务提交接口 (Task Submission Interface): 允许用户向线程池提交任务。
⚝ 线程管理策略 (Thread Management Policy): 控制线程池中线程的数量和生命周期。
当用户通过任务提交接口向 ThreadPoolExecutor
提交任务时,任务会被放入任务队列。线程池中的工作线程会不断地从任务队列中取出任务并执行。如果任务队列为空,工作线程会进入等待状态;当有新的任务加入队列时,等待的线程会被唤醒并继续执行任务。
② 创建 ThreadPoolExecutor
创建 folly::ThreadPoolExecutor
对象通常需要指定线程池的最大线程数。例如,创建一个最大线程数为 4 的线程池:
1
#include <folly/executors/ThreadPoolExecutor.h>
2
#include <folly/executors/task_queue/LifoSemMPMCQueue.h> // 显式包含 LifoSemMPMCQueue
3
#include <iostream>
4
5
int main() {
6
// 使用默认的任务队列 (默认是 MPMCQueue) 和默认的线程工厂
7
auto executor = std::make_shared<folly::ThreadPoolExecutor>(4);
8
// ... 后续使用 executor 提交任务 ...
9
return 0;
10
}
在上面的代码中,std::make_shared<folly::ThreadPoolExecutor>(4)
创建了一个 ThreadPoolExecutor
对象,并设置最大线程数为 4。 folly::ThreadPoolExecutor
的构造函数还接受其他可选参数,用于更精细地配置线程池的行为,例如任务队列类型、线程工厂等。
③ 提交任务到线程池
向 ThreadPoolExecutor
提交任务通常使用 add()
方法。add()
方法接受一个 folly::Func
对象作为参数,folly::Func
是 Folly 库对函数对象的封装。例如,提交一个简单的 lambda 函数:
1
executor->add([] {
2
std::cout << "Task executed by thread: " << std::this_thread::get_id() << std::endl;
3
});
add()
方法是非阻塞的,它会将任务放入任务队列后立即返回。任务会在线程池中的某个线程空闲时被执行。
④ 等待所有任务完成
如果你需要等待线程池中的所有任务都执行完成后再继续执行后续操作,可以调用 executor->join()
方法。join()
方法会阻塞当前线程,直到线程池中的任务队列为空,并且所有正在执行的任务都已完成。
1
executor->join(); // 等待线程池中所有任务完成
2
std::cout << "All tasks finished." << std::endl;
⑤ 线程池的配置选项
folly::ThreadPoolExecutor
提供了丰富的配置选项,允许你根据不同的应用场景进行优化。一些常用的配置选项包括:
⚝ 最大线程数 (Maximum Pool Size): 通过构造函数参数指定,决定线程池中最多可以创建多少个线程。
⚝ 任务队列类型 (Task Queue Type): 可以通过构造函数参数指定任务队列的类型。Folly 提供了多种任务队列实现,例如 MPMCQueue
(Multi-Producer Multi-Consumer Queue, 多生产者多消费者队列)、LifoSemMPMCQueue
(LIFO 语义的 MPMC 队列) 等。不同的任务队列类型在性能和特性上有所差异,可以根据具体需求选择。
⚝ 线程工厂 (Thread Factory): 用于创建线程池中的工作线程。你可以自定义线程工厂,例如设置线程的栈大小、优先级等。
⚝ 拒绝策略 (Rejection Policy): 当任务队列已满,并且线程池已达到最大线程数时,新的任务将被拒绝。拒绝策略定义了如何处理被拒绝的任务。Folly 提供了多种拒绝策略,例如抛出异常、丢弃任务、在提交线程中执行任务等。
⑥ 任务队列类型
folly::ThreadPoolExecutor
支持多种任务队列类型,常见的有:
⚝ folly::MPMCQueue
(默认): 多生产者多消费者队列,具有较高的吞吐量和较低的延迟,适用于大多数场景。
⚝ folly::LifoSemMPMCQueue
: 后进先出 (LIFO) 语义的 MPMC 队列。LIFO 队列可以提高缓存局部性,在某些场景下可以提升性能。
⚝ folly::PriorityQueue
: 优先级队列。任务可以设置优先级,优先级高的任务会优先被执行。
你可以通过 ThreadPoolExecutor::Options
结构体来配置任务队列类型。例如,使用 LifoSemMPMCQueue
作为任务队列:
1
#include <folly/executors/ThreadPoolExecutor.h>
2
#include <folly/executors/task_queue/LifoSemMPMCQueue.h>
3
4
int main() {
5
folly::ThreadPoolExecutor::Options options;
6
options.taskQueue = std::make_unique<folly::LifoSemMPMCQueue>(); // 设置任务队列为 LifoSemMPMCQueue
7
auto executor = std::make_shared<folly::ThreadPoolExecutor>(options, 4);
8
// ... 后续使用 executor ...
9
return 0;
10
}
注意: 你需要显式包含任务队列的头文件 (例如 #include <folly/executors/task_queue/LifoSemMPMCQueue.h>
),即使你使用了 ThreadPoolExecutor::Options
来配置。
⑦ 选择合适的线程池配置
选择合适的线程池配置需要根据具体的应用场景和性能需求进行权衡。一些通用的原则包括:
⚝ 最大线程数: 通常设置为 CPU 核心数的几倍,具体数值需要根据任务的 CPU 密集程度和 I/O 密集程度进行调整。对于 CPU 密集型任务,线程数不宜设置过高,否则会造成过多的线程切换开销。对于 I/O 密集型任务,可以适当增加线程数,以提高并发度。
⚝ 任务队列类型: 默认的 MPMCQueue
适用于大多数场景。如果任务之间存在较强的局部性,可以考虑使用 LifoSemMPMCQueue
。如果需要支持任务优先级,可以使用 PriorityQueue
。
⚝ 拒绝策略: 根据应用的需求选择合适的拒绝策略。如果任务丢失是不可接受的,可以抛出异常或在提交线程中执行任务。如果可以容忍少量任务丢失,可以丢弃任务。
⑧ 总结
folly::ThreadPoolExecutor
是一个功能强大、高度可配置的线程池实现。它提供了丰富的配置选项,允许你根据不同的应用场景进行优化。合理地使用线程池可以显著提高并发程序的性能和资源利用率。在需要执行大量异步任务或需要控制并发度的场景下,ThreadPoolExecutor
是一个非常有价值的工具。
6.1.3 线程局部存储 (Thread-Local Storage)
介绍线程局部存储的概念和 Folly 库提供的线程局部存储工具,以及如何在多线程环境中安全地访问线程私有数据。
线程局部存储 (Thread-Local Storage, TLS) 是一种特殊的存储机制,它允许每个线程拥有自己独立的变量副本。即使多个线程访问同一个线程局部变量,它们访问的也是各自线程私有的副本,互不影响。线程局部存储主要用于解决多线程环境中的数据隔离和线程安全问题。
① 线程局部存储的概念
在传统的多线程编程中,全局变量和静态变量是所有线程共享的。如果多个线程同时读写这些共享变量,就需要使用互斥锁等同步机制来保证线程安全。然而,频繁的锁竞争会降低程序的性能。线程局部存储提供了一种避免锁竞争的方案,通过为每个线程提供独立的变量副本,使得线程可以安全地访问和修改自己的数据,而无需担心与其他线程的冲突。
② folly::ThreadLocal
folly::ThreadLocal<T>
是 Folly 库提供的线程局部存储模板类,用于创建线程局部变量。T
是变量的类型。
声明一个线程局部变量:
1
#include <folly/ThreadLocal.h>
2
#include <iostream>
3
#include <thread>
4
5
folly::ThreadLocal<int> tls_counter; // 声明一个线程局部 int 变量
6
7
void thread_function() {
8
tls_counter = 0; // 初始化当前线程的 tls_counter
9
for (int i = 0; i < 5; ++i) {
10
tls_counter++;
11
std::cout << "Thread ID: " << std::this_thread::get_id() << ", counter: " << tls_counter.get() << std::endl;
12
}
13
}
14
15
int main() {
16
std::thread t1(thread_function);
17
std::thread t2(thread_function);
18
19
t1.join();
20
t2.join();
21
22
return 0;
23
}
在这个例子中,tls_counter
是一个 folly::ThreadLocal<int>
类型的线程局部变量。每个线程执行 thread_function
时,都会访问到自己独立的 tls_counter
副本。因此,线程 t1 和线程 t2 对 tls_counter
的修改互不影响,输出结果会显示每个线程的计数器都是从 1 增长到 5。
③ folly::ThreadLocalPtr
folly::ThreadLocalPtr<T>
是另一种线程局部存储模板类,用于存储指针类型的数据。与 folly::ThreadLocal<T>
不同的是,folly::ThreadLocalPtr<T>
存储的是指向类型 T
对象的指针,而不是对象本身。
声明一个线程局部指针变量:
1
#include <folly/ThreadLocal.h>
2
#include <iostream>
3
#include <thread>
4
5
struct MyData {
6
int value;
7
MyData(int v) : value(v) {}
8
};
9
10
folly::ThreadLocalPtr<MyData> tls_data_ptr; // 声明一个线程局部 MyData 指针
11
12
void thread_function_ptr() {
13
tls_data_ptr.reset(new MyData(10)); // 为当前线程分配 MyData 对象并存储指针
14
std::cout << "Thread ID: " << std::this_thread::get_id() << ", data value: " << tls_data_ptr->value << std::endl;
15
tls_data_ptr.reset(); // 释放当前线程的 MyData 对象
16
}
17
18
int main() {
19
std::thread t3(thread_function_ptr);
20
std::thread t4(thread_function_ptr);
21
22
t3.join();
23
t4.join();
24
25
return 0;
26
}
在这个例子中,tls_data_ptr
是一个 folly::ThreadLocalPtr<MyData>
类型的线程局部指针变量。每个线程在 thread_function_ptr
中都会分配一个 MyData
对象,并将指针存储在 tls_data_ptr
中。使用 tls_data_ptr.reset(new MyData(10))
来分配和设置指针,使用 tls_data_ptr.reset()
来释放内存并将指针置空。
④ 初始化和销毁
folly::ThreadLocal<T>
和 folly::ThreadLocalPtr<T>
提供了多种初始化方式。默认情况下,线程局部变量会在线程首次访问时进行默认初始化 (对于 folly::ThreadLocal<T>
,使用类型 T
的默认构造函数;对于 folly::ThreadLocalPtr<T>
,初始化为 null)。
你也可以自定义初始化函数,在线程首次访问线程局部变量时调用。例如,使用 lambda 函数作为初始化器:
1
folly::ThreadLocal<int> tls_counter_init([] {
2
std::cout << "Initializing tls_counter for thread: " << std::this_thread::get_id() << std::endl;
3
return 100; // 初始化为 100
4
});
5
6
void thread_function_init() {
7
std::cout << "Thread ID: " << std::this_thread::get_id() << ", initial counter: " << tls_counter_init.get() << std::endl;
8
}
对于 folly::ThreadLocalPtr<T>
,你可以在 reset()
方法中传入一个删除器 (deleter) 函数,用于自定义对象销毁时的操作。
⑤ 使用场景
线程局部存储在以下场景中非常有用:
⚝ 线程安全: 当需要在多线程环境中访问和修改某个变量,但又不想使用互斥锁等同步机制时,可以使用线程局部存储。每个线程拥有独立的变量副本,避免了数据竞争和锁竞争。
⚝ 上下文传递: 在复杂的异步调用链中,可以使用线程局部存储来传递线程相关的上下文信息,例如请求 ID、用户 ID 等。
⚝ 性能优化: 对于频繁读取但不常修改的数据,可以使用线程局部存储来缓存数据,提高访问速度。
⑥ 注意事项
⚝ 内存开销: 线程局部存储会增加内存开销,因为每个线程都需要维护一份变量副本。如果线程局部变量占用较多内存,并且线程数量很大,可能会消耗大量内存。
⚝ 生命周期管理: 对于 folly::ThreadLocalPtr<T>
,需要手动管理指针指向的对象的生命周期,避免内存泄漏。使用 reset()
方法可以释放之前分配的对象。
⚝ 滥用: 线程局部存储虽然可以简化多线程编程,但也不应滥用。过度使用线程局部存储可能会导致代码难以理解和维护。只在真正需要线程隔离的场景下才考虑使用线程局部存储。
⑦ 总结
folly::ThreadLocal<T>
和 folly::ThreadLocalPtr<T>
是 Folly 库提供的线程局部存储工具,用于创建线程私有的变量副本。线程局部存储可以简化多线程编程,提高线程安全性和性能,特别是在需要数据隔离和避免锁竞争的场景下。合理地使用线程局部存储可以提升并发程序的效率和可维护性。
6.2 互斥锁与条件变量 (Mutexes and Condition Variables)
介绍 Folly 库提供的互斥锁 (Mutex) 和条件变量 (ConditionVariable),讲解如何使用它们实现线程同步和互斥访问共享资源。
互斥锁 (Mutex) 和条件变量 (Condition Variable) 是并发编程中常用的同步原语,用于控制多个线程对共享资源的访问,以及实现线程间的协作和通信。Folly 库提供了 folly::Mutex
、folly::SharedMutex
(共享互斥锁) 和 folly::ConditionVariable
等工具,用于构建线程安全、高效的并发程序。
6.2.1 folly::Mutex 与 folly::SharedMutex:互斥锁的实现 (folly::Mutex and folly::SharedMutex: Mutex Implementations)
讲解 folly::Mutex
和 folly::SharedMutex
的 API 和用法,以及它们之间的区别和适用场景。
互斥锁 (Mutex, Mutual Exclusion Lock) 是一种基本的同步机制,用于保护共享资源,防止多个线程同时访问,从而避免数据竞争 (data race) 和其他并发问题。folly::Mutex
和 folly::SharedMutex
是 Folly 库提供的两种互斥锁实现,它们都提供了互斥访问的功能,但在使用场景和特性上有所不同。
① folly::Mutex
(独占锁)
folly::Mutex
是一种独占锁,也称为排他锁。当一个线程获取 (lock) 了 folly::Mutex
后,其他线程必须等待该线程释放 (unlock) 锁才能获取。folly::Mutex
适用于需要对共享资源进行独占访问的场景,例如修改共享数据、更新状态等。
基本 API:
⚝ lock()
: 尝试获取互斥锁。如果互斥锁当前未被其他线程持有,则当前线程成功获取锁并继续执行;否则,当前线程会被阻塞,直到互斥锁变为可用。
⚝ unlock()
: 释放互斥锁。释放锁后,其他等待获取锁的线程可能会被唤醒并尝试获取锁。
⚝ try_lock()
: 尝试非阻塞地获取互斥锁。如果互斥锁当前可用,则当前线程成功获取锁并返回 true
;否则,立即返回 false
,不会阻塞。
⚝ ൺlock_guard<folly::Mutex>
: RAII (Resource Acquisition Is Initialization) 风格的互斥锁包装器。在构造时获取锁,在析构时自动释放锁,确保互斥锁的正确释放,即使在发生异常的情况下也能保证。
示例: 使用 folly::Mutex
保护共享计数器
1
#include <folly/synchronization/Mutex.h>
2
#include <iostream>
3
#include <thread>
4
5
folly::Mutex mutex; // 声明一个互斥锁
6
int shared_counter = 0;
7
8
void increment_counter() {
9
for (int i = 0; i < 100000; ++i) {
10
folly::ൺlock_guard<folly::Mutex> lock(mutex); // 获取互斥锁
11
shared_counter++; // 临界区:访问共享计数器
12
}
13
}
14
15
int main() {
16
std::thread t1(increment_counter);
17
std::thread t2(increment_counter);
18
19
t1.join();
20
t2.join();
21
22
std::cout << "Shared counter value: " << shared_counter << std::endl; // 预期输出:200000
23
24
return 0;
25
}
在这个例子中,folly::Mutex mutex
用于保护共享变量 shared_counter
。folly::ൺlock_guard<folly::Mutex> lock(mutex);
在 increment_counter
函数中创建了一个互斥锁包装器 lock
。当 lock
对象被创建时,会自动调用 mutex.lock()
获取互斥锁。当 increment_counter
函数执行完成或发生异常退出时,lock
对象析构,会自动调用 mutex.unlock()
释放互斥锁。这样确保了在任何情况下,互斥锁都能被正确释放,避免死锁。
② folly::SharedMutex
(共享锁)
folly::SharedMutex
是一种共享互斥锁,也称为读写锁 (Read-Write Lock)。它允许多个线程同时获取共享模式 (shared mode, 读模式) 的锁,但只允许一个线程获取独占模式 (exclusive mode, 写模式) 的锁。folly::SharedMutex
适用于读多写少的场景,可以提高并发度。
基本 API:
⚝ lock_shared()
: 获取共享模式的锁。多个线程可以同时持有共享锁。
⚝ unlock_shared()
: 释放共享模式的锁。
⚝ lock()
: 获取独占模式的锁。当线程持有独占锁时,其他线程无法获取共享锁或独占锁。
⚝ unlock()
: 释放独占模式的锁。
⚝ try_lock_shared()
, try_lock()
: 非阻塞地尝试获取共享锁和独占锁。
⚝ ൺlock_guard<folly::SharedMutex>
(独占模式), Sharedൺlock_guard<folly::SharedMutex>
(共享模式): RAII 风格的锁包装器,分别用于独占模式和共享模式。
示例: 使用 folly::SharedMutex
保护读多写少的共享数据
1
#include <folly/synchronization/SharedMutex.h>
2
#include <iostream>
3
#include <thread>
4
#include <vector>
5
#include <numeric>
6
7
folly::SharedMutex shared_data_mutex; // 声明一个共享互斥锁
8
std::vector<int> shared_data = {1, 2, 3, 4, 5};
9
10
int calculate_sum() {
11
folly::Sharedൺlock_guard<folly::SharedMutex> lock(shared_data_mutex); // 获取共享锁 (读模式)
12
return std::accumulate(shared_data.begin(), shared_data.end(), 0); // 临界区:读取共享数据
13
}
14
15
void update_data(int index, int value) {
16
folly::ൺlock_guard<folly::SharedMutex> lock(shared_data_mutex); // 获取独占锁 (写模式)
17
if (index >= 0 && index < shared_data.size()) {
18
shared_data[index] = value; // 临界区:修改共享数据
19
}
20
}
21
22
int main() {
23
std::vector<std::thread> readers;
24
for (int i = 0; i < 4; ++i) {
25
readers.emplace_back(calculate_sum); // 4 个读线程
26
}
27
28
std::thread writer(update_data, 2, 10); // 1 个写线程
29
30
for (auto& reader_thread : readers) {
31
reader_thread.join();
32
}
33
writer.join();
34
35
std::cout << "Data after update: ";
36
for (int val : shared_data) {
37
std::cout << val << " ";
38
}
39
std::cout << std::endl; // 预期输出包含 10
40
41
return 0;
42
}
在这个例子中,folly::SharedMutex shared_data_mutex
用于保护共享数据 shared_data
。calculate_sum
函数使用 folly::Sharedൺlock_guard<folly::SharedMutex>
获取共享锁,允许多个读线程同时访问 shared_data
。update_data
函数使用 folly::ൺlock_guard<folly::SharedMutex>
获取独占锁,确保在写操作时没有其他线程同时访问 shared_data
。
③ folly::RWൺlockGuard
(读写锁包装器)
folly::RWൺlockGuard
是一个更通用的读写锁包装器,它可以根据模板参数选择获取读锁或写锁。
1
template <typename MutexType, bool exclusive>
2
class RWൺlockGuard;
⚝ MutexType
: 锁的类型,例如 folly::SharedMutex
。
⚝ exclusive
: 布尔值,true
表示获取独占锁 (写锁),false
表示获取共享锁 (读锁)。
使用 folly::RWൺlockGuard
可以更灵活地控制锁的模式。
④ 适用场景
⚝ folly::Mutex
: 适用于需要独占访问共享资源的场景,例如对共享数据进行修改、更新状态等。在竞争不激烈的情况下,folly::Mutex
的性能通常足够好。
⚝ folly::SharedMutex
: 适用于读多写少的场景。共享锁允许多个读线程并发访问共享资源,提高并发度。当写操作较少时,使用 folly::SharedMutex
可以显著提升性能。
⚝ 选择: 如果共享资源的访问模式以写操作为主,或者读写操作比例接近,folly::Mutex
通常是更好的选择,因为 folly::SharedMutex
在写操作时可能会有更高的开销。只有在读操作远多于写操作的情况下,folly::SharedMutex
才能体现出优势。
⑤ 性能考虑
⚝ 锁竞争: 互斥锁的性能瓶颈通常在于锁竞争。当多个线程频繁地争夺同一个锁时,会导致线程阻塞和上下文切换,降低程序的性能。
⚝ 锁粒度: 锁粒度 (lock granularity) 指的是锁保护的资源范围。粗粒度锁 (coarse-grained lock) 保护的资源范围较大,锁竞争的概率较高,但实现简单。细粒度锁 (fine-grained lock) 保护的资源范围较小,锁竞争的概率较低,但实现复杂。选择合适的锁粒度是性能优化的关键。
⚝ 无锁编程: 在某些高性能要求的场景下,可以考虑使用无锁编程 (lock-free programming) 技术,例如原子操作、无锁数据结构等,来避免锁竞争的开销。我们将在后续小节中介绍原子操作和无锁编程。
⑥ 总结
folly::Mutex
和 folly::SharedMutex
是 Folly 库提供的互斥锁实现,用于保护共享资源,实现线程同步。folly::Mutex
提供独占访问,适用于通用互斥场景。folly::SharedMutex
提供共享模式和独占模式,适用于读多写少的场景。选择合适的互斥锁类型和锁粒度,以及在必要时考虑无锁编程技术,是构建高性能并发程序的关键。
6.2.2 folly::ConditionVariable:条件变量的实现 (folly::ConditionVariable: Condition Variable Implementation)
介绍 folly::ConditionVariable
的 API 和用法,以及如何使用条件变量实现线程间的等待和通知。
条件变量 (Condition Variable) 是一种同步原语,通常与互斥锁 (Mutex) 结合使用,用于实现线程间的等待和通知机制。条件变量允许线程在满足特定条件时挂起等待,并在条件满足时被其他线程唤醒。folly::ConditionVariable
是 Folly 库提供的条件变量实现。
① 条件变量的基本概念
条件变量本身不保护任何共享资源,它必须与一个互斥锁关联使用。条件变量提供以下主要操作:
⚝ 等待 (Wait): 线程调用条件变量的等待操作时,会原子性地释放关联的互斥锁并进入等待状态。等待状态的线程会被阻塞,直到被其他线程唤醒。
⚝ 通知 (Notify): 当某个线程修改了共享资源,使得等待条件可能满足时,可以调用条件变量的通知操作来唤醒一个或多个等待的线程。被唤醒的线程会重新尝试获取互斥锁,并在获取锁后检查等待条件是否真的满足。
注意: 条件变量的等待操作必须在持有互斥锁的情况下调用,并且在等待返回后需要重新检查等待条件,这被称为“Spurious Wakeup” (虚假唤醒) 的处理。
② folly::ConditionVariable
的基本 API
⚝ wait(ൺlock_guard<Mutex>& lock)
: 等待条件变量。当前线程必须持有互斥锁 lock
。wait()
操作会原子性地释放 lock
并使当前线程进入等待状态。当线程被唤醒时,wait()
操作会重新尝试获取互斥锁,并在获取锁后返回。
⚝ wait_for(ൺlock_guard<Mutex>& lock, std::chrono::duration<Rep, Period> const& timeout_duration)
: 带超时时间的等待。与 wait()
类似,但等待时间超过 timeout_duration
后,即使没有被通知,wait_for()
也会返回 (可能返回超时或虚假唤醒)。
⚝ wait_until(ൺlock_guard<Mutex>& lock, std::chrono::time_point<Clock, Duration> const& timeout_time)
: 等待到指定时间点的等待。与 wait_for()
类似,但超时时间以时间点表示。
⚝ notify_one()
: 唤醒一个等待在条件变量上的线程。如果有多个线程在等待,具体唤醒哪个线程由调度器决定。
⚝ notify_all()
: 唤醒所有等待在条件变量上的线程。
③ 使用 folly::ConditionVariable
实现生产者-消费者模型
生产者-消费者模型 (Producer-Consumer Pattern) 是并发编程中常见的模式,用于解耦生产者线程和消费者线程,提高数据处理效率。我们可以使用 folly::ConditionVariable
和 folly::Mutex
实现一个简单的生产者-消费者模型。
代码示例:
1
#include <folly/synchronization/ConditionVariable.h>
2
#include <folly/synchronization/Mutex.h>
3
#include <iostream>
4
#include <queue>
5
#include <thread>
6
#include <chrono>
7
8
folly::Mutex mutex; // 互斥锁,保护共享队列
9
folly::ConditionVariable cv; // 条件变量
10
std::queue<int> task_queue; // 共享任务队列
11
const int kMaxQueueSize = 5;
12
13
void producer() {
14
for (int i = 1; ; ++i) {
15
{
16
folly::ൺlock_guard<folly::Mutex> lock(mutex); // 获取互斥锁
17
while (task_queue.size() >= kMaxQueueSize) {
18
cv.wait(lock); // 队列已满,生产者等待
19
}
20
task_queue.push(i);
21
std::cout << "Produced task: " << i << std::endl;
22
}
23
cv.notify_one(); // 通知消费者有新任务
24
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产耗时
25
}
26
}
27
28
void consumer() {
29
while (true) {
30
int task_id;
31
{
32
folly::ൺlock_guard<folly::Mutex> lock(mutex); // 获取互斥锁
33
while (task_queue.empty()) {
34
cv.wait(lock); // 队列为空,消费者等待
35
}
36
task_id = task_queue.front();
37
task_queue.pop();
38
std::cout << "Consumed task: " << task_id << std::endl;
39
}
40
cv.notify_one(); // 通知生产者队列有空位
41
std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 模拟消费耗时
42
}
43
}
44
45
int main() {
46
std::thread producer_thread(producer);
47
std::thread consumer_thread(consumer);
48
49
producer_thread.detach();
50
consumer_thread.detach();
51
52
std::this_thread::sleep_for(std::chrono::seconds(10)); // 运行一段时间后结束
53
54
return 0;
55
}
在这个生产者-消费者模型的例子中:
⚝ mutex
用于保护共享任务队列 task_queue
的线程安全访问。
⚝ cv
是条件变量,用于在队列为空或满时,实现生产者和消费者线程的等待和通知。
⚝ 生产者线程 (producer
): 不断生产任务 (整数),当队列已满时,调用 cv.wait(lock)
挂起等待,直到消费者消费任务后被唤醒。生产完任务后,调用 cv.notify_one()
通知消费者有新任务。
⚝ 消费者线程 (consumer
): 不断消费任务,当队列为空时,调用 cv.wait(lock)
挂起等待,直到生产者生产任务后被唤醒。消费完任务后,调用 cv.notify_one()
通知生产者队列有空位。
关键点:
⚝ 互斥锁保护: 条件变量的等待和通知操作必须在持有互斥锁的情况下进行,以保证操作的原子性。
⚝ 等待条件: 等待线程被唤醒后,需要重新检查等待条件是否真的满足,因为可能存在虚假唤醒。在 while
循环中检查条件是常用的做法。
⚝ 通知时机: 生产者在生产任务后,消费者在消费任务后,都需要调用 cv.notify_one()
或 cv.notify_all()
通知等待的线程。
④ 虚假唤醒 (Spurious Wakeup)
虚假唤醒是指线程在没有被显式通知的情况下,从条件变量的等待状态被唤醒。虚假唤醒是可能发生的 (尤其是在某些操作系统或硬件平台上),因此在条件变量的等待循环中,必须始终检查等待条件是否真的满足。
正确的条件变量等待循环模式:
1
{
2
folly::ൺlock_guard<folly::Mutex> lock(mutex);
3
while (!condition_is_met) { // 循环检查条件
4
cv.wait(lock); // 等待条件变量
5
}
6
// ... 条件满足,执行临界区代码 ...
7
}
⑤ notify_one()
vs. notify_all()
⚝ notify_one()
: 唤醒一个等待线程。适用于只需要唤醒一个线程来处理任务的场景,例如生产者-消费者模型中,一个生产者生产的任务只需要一个消费者消费。
⚝ notify_all()
: 唤醒所有等待线程。适用于多个线程都需要被唤醒并重新检查条件的场景,例如多个消费者线程竞争消费任务,或者多个线程等待同一事件发生。
选择 notify_one()
还是 notify_all()
需要根据具体的应用场景和需求来决定。notify_one()
的开销通常比 notify_all()
小,但如果唤醒的线程不满足条件,可能会导致额外的线程调度开销。
⑥ 总结
folly::ConditionVariable
是 Folly 库提供的条件变量实现,用于实现线程间的等待和通知机制。条件变量通常与互斥锁结合使用,用于构建复杂的线程同步和协作逻辑,例如生产者-消费者模型、事件通知等。正确地使用条件变量,并注意处理虚假唤醒问题,可以构建高效、可靠的并发程序。
6.2.3 死锁的预防与避免 (Deadlock Prevention and Avoidance)
讲解死锁的概念、产生原因和预防方法,以及如何在并发编程中避免死锁。
死锁 (Deadlock) 是并发编程中一种常见的错误状态,指两个或多个线程因互相等待对方释放资源而无限期地阻塞,导致程序无法继续执行。死锁是并发程序中需要极力避免的严重问题。
① 死锁的概念和产生原因
死锁的定义: 一组线程发生死锁是指:
⚝ 每个线程都在等待一个事件,而这个事件只能由这组线程中的其他线程触发。
⚝ 没有线程能够被唤醒,因此所有线程都将永远阻塞下去。
死锁产生的四个必要条件 (Coffman 条件): 只有当以下四个条件同时满足时,才会发生死锁:
- 互斥条件 (Mutual Exclusion): 至少有一个资源必须处于独占模式,即一次只有一个线程可以使用该资源。其他线程如果请求该资源,必须等待。互斥锁和独占锁都满足互斥条件。
- 占有并等待条件 (Hold and Wait): 一个线程至少持有一个资源,并且同时还在请求其他线程占有的资源。
- 不可剥夺条件 (No Preemption): 线程已获得的资源在未使用完之前,不能被强制剥夺,只能由持有它的线程主动释放。互斥锁和条件变量等同步原语通常都满足不可剥夺条件。
- 循环等待条件 (Circular Wait): 存在一个线程等待资源的环路链,例如,线程 A 等待线程 B 占有的资源,线程 B 等待线程 C 占有的资源,...,线程 Z 等待线程 A 占有的资源,形成一个环形等待链。
只要上述四个条件同时成立,就可能发生死锁。反之,只要破坏其中一个或多个条件,就可以预防死锁。
② 死锁的预防方法
死锁预防的目标是破坏死锁产生的四个必要条件中的一个或多个,从而避免死锁的发生。
- 破坏互斥条件: 在某些情况下,可以尝试将独占资源转换为共享资源,例如使用读写锁代替互斥锁,允许多个线程同时读取资源。但互斥条件通常是资源本身的属性,很难完全消除。
- 破坏占有并等待条件: 避免线程在持有资源的同时请求其他资源。一种常见的做法是一次性请求所有需要的资源。如果线程需要多个资源,在开始操作前一次性申请所有资源,如果任何一个资源无法获取,则释放已获取的所有资源,稍后重新尝试。但这可能会导致资源利用率降低和线程饥饿 (starvation) 问题。
- 破坏不可剥夺条件: 允许操作系统或资源管理系统强制剥夺线程已获得的资源。但这在实际应用中实现较为复杂,且可能会导致数据一致性问题。对于互斥锁等同步原语,通常不适用剥夺策略。
- 破坏循环等待条件: 资源排序 (Resource Ordering) 是破坏循环等待条件常用的方法。为所有资源定义一个全局的顺序,要求线程按照资源顺序依次请求资源。例如,如果线程需要同时获取锁 A 和锁 B,并且锁 A 的顺序在锁 B 之前,则线程必须先获取锁 A,再获取锁 B。通过资源排序,可以避免循环等待链的形成。
③ 死锁的避免方法
死锁避免 (Deadlock Avoidance) 是在程序运行时动态地检测可能导致死锁的操作,并采取措施避免死锁的发生。死锁避免通常需要系统或资源管理器具有更多的信息,例如资源分配状态、线程的资源请求序列等。
银行家算法 (Banker's Algorithm) 是一种经典的死锁避免算法。银行家算法模拟银行贷款的过程,根据线程的最大资源需求量和当前资源分配情况,判断是否允许线程继续请求资源。只有在系统处于安全状态 (safe state) 时,才允许线程请求资源。安全状态是指系统能够找到一个线程执行序列,使得所有线程都能顺利完成。
死锁避免算法通常实现较为复杂,并且可能会带来额外的运行时开销。在实际应用中,死锁预防方法通常比死锁避免方法更常用。
④ 常见的死锁场景和预防技巧
⚝ 嵌套锁 (Nested Locks): 当线程在持有锁 A 的情况下,又尝试获取锁 B,而另一个线程持有锁 B 并尝试获取锁 A 时,就可能发生死锁。预防方法:
▮▮▮▮⚝ 避免嵌套锁: 尽量减少锁的嵌套层数,简化锁的获取逻辑。
▮▮▮▮⚝ 资源排序: 如果必须使用嵌套锁,使用资源排序方法,确保所有线程按照相同的顺序获取锁。
⚝ 死锁检测与恢复: 如果死锁预防和避免方法无法完全消除死锁风险,可以考虑使用死锁检测 (Deadlock Detection) 和死锁恢复 (Deadlock Recovery) 技术。死锁检测是指定期或在特定事件发生时,检测系统中是否发生死锁。死锁恢复是指在检测到死锁后,采取措施解除死锁,例如终止一个或多个死锁线程,或者回滚事务等。死锁检测和恢复通常实现较为复杂,并且可能会带来性能开销和数据一致性问题。
⑤ 死锁预防的最佳实践
⚝ 尽量减少锁的使用: 优先考虑使用无锁数据结构和算法,或者使用更细粒度的锁,减少锁竞争和死锁的可能性。
⚝ 保持锁的持有时间尽可能短: 只在必要时才获取锁,并在临界区代码执行完成后尽快释放锁。避免在持有锁的情况下执行耗时操作,例如 I/O 操作、网络通信等。
⚝ 使用 RAII 风格的锁包装器: 例如 folly::ൺlock_guard
和 folly::Sharedൺlock_guard
,确保锁的正确释放,即使在发生异常的情况下也能保证。
⚝ 避免在回调函数中获取锁: 回调函数可能会在持有锁的情况下被调用,如果回调函数中又尝试获取相同的锁或另一个锁,容易导致死锁。
⚝ 仔细设计锁的获取顺序: 对于需要获取多个锁的场景,仔细设计锁的获取顺序,并使用资源排序等方法预防死锁。
⚝ 进行充分的测试: 并发程序容易出现死锁等难以调试的错误,进行充分的测试,包括单元测试、集成测试和压力测试,有助于发现和修复死锁问题。
⑥ 总结
死锁是并发编程中需要极力避免的严重问题。理解死锁的概念、产生原因和必要条件,掌握死锁预防和避免方法,并遵循死锁预防的最佳实践,可以有效地降低死锁发生的风险,提高并发程序的可靠性和稳定性。在复杂的并发系统中,死锁预防通常比死锁避免更实用和高效。
6.3 原子操作与无锁编程 (Atomic Operations and Lock-Free Programming)
介绍 Folly 库提供的原子操作工具,讲解如何使用原子操作实现无锁数据结构和算法,提高并发性能。
原子操作 (Atomic Operations) 是指不可中断的操作。在执行原子操作的过程中,不会发生线程切换,也不会被其他线程干扰。原子操作是实现无锁编程 (Lock-Free Programming) 的基础。无锁编程是一种高级的并发编程技术,旨在避免使用互斥锁等传统的同步机制,从而提高并发程序的性能和可扩展性。Folly 库提供了丰富的原子操作工具,例如 folly::AtomicHashMap
和原子类型,用于构建高效的无锁数据结构和算法。
6.3.1 folly::AtomicHashMap:原子哈希容器 (folly::AtomicHashMap: Atomic Hash Container)
介绍 folly::AtomicHashMap
的设计和实现,以及如何在并发环境中安全地使用原子哈希容器。
folly::AtomicHashMap<Key, Value>
是 Folly 库提供的一个高性能的无锁哈希容器。它使用原子操作和无锁算法实现,允许多个线程并发地进行插入、删除、查找等操作,而无需使用互斥锁。folly::AtomicHashMap
适用于高并发、读多写少的场景,可以提供比基于互斥锁的哈希容器更高的吞吐量和更低的延迟。
① folly::AtomicHashMap
的设计原理
folly::AtomicHashMap
的核心设计思想是使用无锁哈希表 (Lock-Free Hash Table)。无锁哈希表通常采用以下技术:
⚝ 原子操作: 使用原子操作 (例如 compare-and-swap, fetch-and-add 等) 来保证数据结构操作的原子性。
⚝ CAS (Compare-and-Swap) 操作: CAS 操作是无锁编程中最核心的原子操作之一。CAS 操作包含三个参数:内存地址 V、期望值 A 和新值 B。CAS 操作会原子性地检查内存地址 V 的值是否等于期望值 A,如果相等,则将内存地址 V 的值更新为新值 B,并返回 true;否则,操作失败,返回 false,并且不会修改内存地址 V 的值。
⚝ 链地址法 (Separate Chaining) 或开放寻址法 (Open Addressing): 用于解决哈希冲突。folly::AtomicHashMap
默认使用链地址法,每个哈希桶 (bucket) 维护一个链表,存储哈希到同一个桶的键值对。
⚝ 细粒度同步: 尽量减少同步的范围,只在必要时使用原子操作,避免全局锁。
folly::AtomicHashMap
的具体实现细节比较复杂,涉及到哈希桶的动态扩容、链表的无锁操作、内存管理等。
② folly::AtomicHashMap
的基本 API
folly::AtomicHashMap
提供了类似于 std::unordered_map
的 API,常用的方法包括:
⚝ insert(key, value)
: 插入键值对。如果 key 已存在,则更新 value。
⚝ emplace(key, value)
: 原地构造键值对并插入。
⚝ erase(key)
: 删除指定 key 的键值对。
⚝ find(key)
: 查找指定 key 的键值对。如果找到,返回迭代器;否则,返回 end()
迭代器。
⚝ at(key)
: 查找指定 key 的键值对。如果找到,返回 value 的引用;否则,抛出异常。
⚝ operator[] (key)
: 查找或插入键值对。如果 key 已存在,返回 value 的引用;否则,插入新的键值对,并返回新插入的 value 的引用。
⚝ size()
: 返回哈希容器中键值对的数量。
⚝ empty()
: 判断哈希容器是否为空。
⚝ begin()
, end()
: 返回迭代器,用于遍历哈希容器。
示例: 使用 folly::AtomicHashMap
存储和访问共享数据
1
#include <folly/container/AtomicHashMap.h>
2
#include <iostream>
3
#include <string>
4
#include <thread>
5
#include <vector>
6
7
folly::AtomicHashMap<std::string, int> atomic_map; // 声明一个原子哈希容器
8
9
void access_map(int thread_id) {
10
for (int i = 0; i < 1000; ++i) {
11
std::string key = "key_" + std::to_string(i % 100); // 模拟不同的 key
12
atomic_map[key]++; // 原子性地增加 key 对应的 value
13
int value = atomic_map.at(key); // 原子性地读取 key 对应的 value
14
if (i % 100 == 0) {
15
std::cout << "Thread " << thread_id << ", key: " << key << ", value: " << value << std::endl;
16
}
17
}
18
}
19
20
int main() {
21
std::vector<std::thread> threads;
22
for (int i = 0; i < 4; ++i) {
23
threads.emplace_back(access_map, i); // 4 个线程并发访问 atomic_map
24
}
25
26
for (auto& thread : threads) {
27
thread.join();
28
}
29
30
std::cout << "Final map size: " << atomic_map.size() << std::endl;
31
32
return 0;
33
}
在这个例子中,folly::AtomicHashMap<std::string, int> atomic_map
用于存储共享数据。多个线程并发地访问 atomic_map
,使用 atomic_map[key]++
原子性地增加 key 对应的 value,并使用 atomic_map.at(key)
原子性地读取 value。由于 folly::AtomicHashMap
是无锁的,因此多个线程可以并发地进行读写操作,而无需使用互斥锁。
③ folly::AtomicHashMap
的优势和适用场景
⚝ 高性能: folly::AtomicHashMap
使用无锁算法实现,避免了锁竞争的开销,在高并发、读多写少的场景下,可以提供比基于互斥锁的哈希容器更高的性能。
⚝ 低延迟: 由于没有锁竞争,folly::AtomicHashMap
的操作延迟通常更低,响应速度更快。
⚝ 高吞吐量: 在高并发场景下,folly::AtomicHashMap
可以处理更多的并发请求,提供更高的吞吐量。
适用场景:
⚝ 高并发、读多写少的场景: 例如,缓存系统、统计计数器、在线配置中心等。
⚝ 对性能和延迟敏感的应用: 例如,在线服务、实时系统等。
⚝ 需要避免锁竞争的场景: 例如,某些性能优化的关键路径上,锁竞争是性能瓶颈。
④ folly::AtomicHashMap
的局限性和注意事项
⚝ 实现复杂: 无锁数据结构的实现通常比基于互斥锁的数据结构复杂得多,需要深入理解原子操作和无锁算法。
⚝ 调试困难: 无锁程序的调试通常比基于互斥锁的程序更困难,因为数据竞争和并发错误更难以追踪和复现。
⚝ 并非所有场景都适用: folly::AtomicHashMap
并非适用于所有场景。在写操作频繁或竞争激烈的场景下,无锁算法的性能优势可能不明显,甚至可能不如基于互斥锁的算法。
⚝ 内存开销: 某些无锁数据结构可能会有较高的内存开销,例如为了支持无锁操作,可能需要额外的内存空间或版本信息。
⑤ 选择合适的哈希容器
⚝ std::unordered_map
: 基于互斥锁的哈希容器,线程安全,实现简单,通用性好,适用于大多数场景。在低并发或中等并发场景下,std::unordered_map
的性能通常足够好。
⚝ folly::AtomicHashMap
: 无锁哈希容器,高性能,低延迟,高吞吐量,适用于高并发、读多写少的场景,对性能和延迟敏感的应用。但实现复杂,调试困难,并非所有场景都适用。
⚝ folly::F14ValueMap
, folly::F14FastMap
: Folly 库提供的其他高性能哈希容器,基于不同的哈希算法和实现策略,在某些场景下可能比 std::unordered_map
或 folly::AtomicHashMap
更高效。
选择合适的哈希容器需要根据具体的应用场景、性能需求和开发成本进行权衡。在高并发、性能敏感的场景下,可以考虑使用 folly::AtomicHashMap
或其他高性能哈希容器。在通用场景下,std::unordered_map
通常是一个安全且可靠的选择。
⑥ 总结
folly::AtomicHashMap
是 Folly 库提供的高性能无锁哈希容器,使用原子操作和无锁算法实现,允许多个线程并发地进行读写操作,而无需使用互斥锁。folly::AtomicHashMap
适用于高并发、读多写少的场景,可以提供比基于互斥锁的哈希容器更高的性能和更低的延迟。但无锁编程技术实现复杂,调试困难,并非所有场景都适用。选择合适的哈希容器需要根据具体的应用场景和性能需求进行权衡。
6.3.2 原子操作的内存顺序与一致性 (Memory Ordering and Consistency of Atomic Operations)
深入剖析原子操作的内存顺序和一致性模型,以及如何根据需求选择合适的内存顺序。
在多核处理器和多线程环境中,内存顺序 (Memory Ordering) 和一致性 (Consistency) 是理解原子操作和无锁编程的关键概念。内存顺序定义了原子操作对内存的访问顺序,一致性模型定义了多线程程序中,多个线程对共享内存的视图如何保持一致。C++11 标准引入了原子操作库 <atomic>
,并提供了六种内存顺序选项,允许开发者根据不同的需求选择合适的内存顺序,以优化性能或保证正确性。
① 内存顺序的概念
在单核处理器上,指令通常按程序代码的顺序依次执行。但在多核处理器上,为了提高性能,处理器可能会对指令进行乱序执行 (out-of-order execution)、使用缓存 (cache) 和写缓冲区 (write buffer) 等优化。这些优化可能会导致程序代码的执行顺序与实际的内存访问顺序不一致,从而在多线程程序中引发意想不到的并发问题。
内存顺序指定了原子操作对内存的访问顺序,以及原子操作对其他内存操作的可见性。C++11 标准定义了以下六种内存顺序:
⚝ std::memory_order_relaxed
(宽松顺序): 最宽松的内存顺序。只保证操作的原子性,不保证任何跨线程的同步或顺序性。性能最高,但通常只在非常特殊的情况下使用。
⚝ std::memory_order_consume
(消费顺序): 用于实现依赖关系的同步。当一个线程读取一个原子变量时,使用 consume
顺序,可以保证该线程后续对依赖于该原子变量的内存的访问,发生在其他线程释放该原子变量之前。但由于现代处理器架构的特性,consume
顺序在实践中较少使用,通常可以使用 acquire
顺序代替。
⚝ std::memory_order_acquire
(获取顺序): 用于实现“获取-释放”同步模型中的“获取”操作。当一个线程读取一个原子变量时,使用 acquire
顺序,可以保证在该操作之后的所有读写操作,都发生在其他线程释放该原子变量之后。
⚝ std::memory_order_release
(释放顺序): 用于实现“获取-释放”同步模型中的“释放”操作。当一个线程写入一个原子变量时,使用 release
顺序,可以保证在该操作之前的所有读写操作,都发生在其他线程读取该原子变量之前。
⚝ std::memory_order_acq_rel
(获取-释放顺序): 同时具有 acquire
和 release
顺序的特性,用于同时进行读取和写入的原子操作,例如 fetch_and_add
、exchange
等。
⚝ std::memory_order_seq_cst
(顺序一致性顺序): 最强的内存顺序,也是默认的内存顺序。保证所有原子操作都按照程序代码的顺序依次执行,并且对所有线程都可见。顺序一致性顺序提供了最强的同步和顺序性保证,但性能开销也最高。
② 内存一致性模型
内存一致性模型定义了多线程程序中,多个线程对共享内存的视图如何保持一致。C++11 标准主要基于顺序一致性 (Sequential Consistency) 模型,并提供了更宽松的内存顺序选项,允许开发者在保证程序正确性的前提下,根据需要选择更高效的内存顺序。
顺序一致性: 顺序一致性是最直观、最易于理解的一致性模型。在顺序一致性模型下,所有线程对共享内存的访问都好像按照一个全局唯一的顺序依次执行,并且每个线程的指令执行顺序都与程序代码的顺序一致。顺序一致性模型保证了程序执行结果的确定性和可预测性,但性能开销也相对较高。
“获取-释放”同步模型: acquire
和 release
顺序用于实现“获取-释放”同步模型,这是一种比顺序一致性更宽松,但仍然能保证正确性的同步模型。在“获取-释放”同步模型中,通常使用一个原子变量作为同步标志。线程通过 acquire
顺序读取同步标志,表示“获取”同步;线程通过 release
顺序写入同步标志,表示“释放”同步。“获取-释放”同步模型可以保证在同步点之前的操作的顺序性和可见性,但允许同步点之后的操作乱序执行,从而提高性能。
③ 选择合适的内存顺序
选择合适的内存顺序需要在性能和正确性之间进行权衡。
⚝ std::memory_order_seq_cst
: 默认的内存顺序,最安全、最易于使用,适用于大多数场景。如果对内存顺序不太熟悉,或者需要保证程序执行结果的确定性和可预测性,建议使用 seq_cst
顺序。
⚝ std::memory_order_acquire
, std::memory_order_release
, std::memory_order_acq_rel
: 用于实现“获取-释放”同步模型,性能比 seq_cst
顺序更高,适用于性能敏感的同步场景,例如自旋锁、无锁数据结构等。但需要仔细分析和设计同步逻辑,确保正确性。
⚝ std::memory_order_relaxed
: 最宽松的内存顺序,性能最高,但只保证操作的原子性,不保证任何跨线程的同步或顺序性。通常只在非常特殊的情况下使用,例如原子计数器,且不依赖于计数器的顺序性。
⚝ std::memory_order_consume
: 在现代处理器架构上,性能优势不明显,通常可以使用 acquire
顺序代替。
选择原则:
⚝ 默认选择 seq_cst
: 除非有明确的性能需求,并且对内存顺序有深入的理解,否则建议使用默认的 seq_cst
顺序,以保证程序的正确性和可移植性。
⚝ 性能优化时考虑 acquire
, release
, acq_rel
: 在性能敏感的同步场景下,可以考虑使用 acquire
、release
、acq_rel
顺序,但需要仔细分析和测试,确保程序在各种平台和编译器下都能正确运行。
⚝ 谨慎使用 relaxed
: relaxed
顺序只保证原子性,不保证同步和顺序性,使用时需要非常谨慎,避免引入难以调试的并发错误。
④ folly::atomic_ref
folly::atomic_ref<T>
是 Folly 库提供的原子引用,允许对非原子类型的对象进行原子操作。folly::atomic_ref<T>
并不存储数据,而是引用一个已存在的类型为 T
的对象,并提供原子操作接口。使用 folly::atomic_ref<T>
可以方便地对共享对象进行原子更新,而无需将对象本身声明为原子类型。
示例: 使用 folly::atomic_ref
原子性地更新共享的 std::vector<int>
1
#include <folly/atomic_ref.h>
2
#include <iostream>
3
#include <vector>
4
#include <thread>
5
6
std::vector<int> shared_vector = {1, 2, 3, 4, 5};
7
folly::atomic_ref<std::vector<int>> atomic_vector_ref(shared_vector); // 创建原子引用
8
9
void update_vector() {
10
std::vector<int> new_vector = {6, 7, 8, 9, 10};
11
atomic_vector_ref.store(new_vector, std::memory_order_seq_cst); // 原子性地更新 vector
12
std::cout << "Vector updated by thread: " << std::this_thread::get_id() << std::endl;
13
}
14
15
void read_vector() {
16
std::vector<int> current_vector = atomic_vector_ref.load(std::memory_order_seq_cst); // 原子性地读取 vector
17
std::cout << "Vector read by thread " << std::this_thread::get_id() << ": ";
18
for (int val : current_vector) {
19
std::cout << val << " ";
20
}
21
std::cout << std::endl;
22
}
23
24
int main() {
25
std::thread writer_thread(update_vector);
26
std::thread reader_thread1(read_vector);
27
std::thread reader_thread2(read_vector);
28
29
writer_thread.join();
30
reader_thread1.join();
31
reader_thread2.join();
32
33
return 0;
34
}
在这个例子中,folly::atomic_ref<std::vector<int>> atomic_vector_ref(shared_vector)
创建了一个原子引用 atomic_vector_ref
,引用了共享的 std::vector<int> shared_vector
。update_vector
函数使用 atomic_vector_ref.store()
原子性地更新 vector,read_vector
函数使用 atomic_vector_ref.load()
原子性地读取 vector。
⑤ 总结
理解原子操作的内存顺序和一致性模型,是进行高效、正确的无锁编程的关键。C++11 标准提供了六种内存顺序选项,允许开发者根据不同的需求选择合适的内存顺序。默认的 seq_cst
顺序最安全、最易于使用,但在性能敏感的场景下,可以考虑使用更宽松的内存顺序,例如 acquire
、release
、acq_rel
顺序,但需要仔细分析和测试,确保程序在各种平台和编译器下都能正确运行。folly::atomic_ref
提供了对非原子类型对象进行原子操作的能力,进一步扩展了原子操作的应用范围。
6.3.3 无锁数据结构的实现案例 (Implementation Cases of Lock-Free Data Structures)
通过示例演示如何使用原子操作实现简单的无锁数据结构,如无锁队列或无锁栈。
无锁数据结构 (Lock-Free Data Structures) 是指在多线程并发访问时,不使用互斥锁等传统同步机制,而是仅使用原子操作来保证数据结构操作的线程安全。无锁数据结构可以避免锁竞争的开销,提高并发程序的性能和可扩展性。本节将通过一个简单的无锁栈 (Lock-Free Stack) 的实现案例,演示如何使用原子操作构建无锁数据结构。
① 无锁栈的基本原理
无锁栈通常使用单链表 (Singly Linked List) 作为底层数据结构,并使用原子操作来保证栈操作 (push, pop) 的线程安全。无锁栈的关键在于原子性地更新栈顶指针 (top pointer)。
数据结构:
1
template <typename T>
2
class LockFreeStack {
3
private:
4
struct Node {
5
T data;
6
Node* next;
7
8
Node(const T& data) : data(data), next(nullptr) {}
9
};
10
11
std::atomic<Node*> head_; // 原子栈顶指针
12
13
public:
14
LockFreeStack() : head_(nullptr) {}
15
16
// ... push, pop, empty 等方法 ...
17
};
head_
是一个原子指针,指向栈顶节点。栈的 push 和 pop 操作需要原子性地修改 head_
指针。
② push
操作的无锁实现
push
操作需要创建一个新节点,并将新节点插入到栈顶。无锁 push
操作的关键在于使用 CAS 操作原子性地更新 head_
指针。
1
void push(const T& data) {
2
Node* new_node = new Node(data);
3
Node* old_head = head_.load(std::memory_order_relaxed); // 1. 读取当前栈顶指针
4
do {
5
new_node->next = old_head; // 2. 将新节点的 next 指针指向当前栈顶
6
} while (!head_.compare_exchange_weak(old_head, new_node, std::memory_order_release, std::memory_order_relaxed)); // 3. CAS 操作更新栈顶指针
7
// 循环 CAS,直到成功
8
}
push
操作的步骤:
- 读取当前栈顶指针
old_head
: 使用head_.load(std::memory_order_relaxed)
原子性地读取当前的栈顶指针。这里使用relaxed
顺序,因为只需要读取指针的值,不需要强同步。 - 设置新节点的
next
指针: 将新节点new_node
的next
指针指向old_head
,即将新节点链接到当前栈顶。 - CAS 操作更新栈顶指针
head_
: 使用head_.compare_exchange_weak(old_head, new_node, std::memory_order_release, std::memory_order_relaxed)
原子性地尝试更新栈顶指针。
▮▮▮▮⚝compare_exchange_weak
: 是 CAS 操作的一种变体,在某些情况下可能比compare_exchange_strong
更高效。
▮▮▮▮⚝old_head
(期望值): CAS 操作期望head_
的当前值等于old_head
。
▮▮▮▮⚝new_node
(新值): 如果 CAS 操作成功,将head_
的值更新为new_node
。
▮▮▮▮⚝std::memory_order_release
(成功时的内存顺序): 使用release
顺序,保证在push
操作之前的所有操作,都发生在其他线程看到新栈顶之前。
▮▮▮▮⚝std::memory_order_relaxed
(失败时的内存顺序): 使用relaxed
顺序,失败时只需要重新读取head_
指针,不需要强同步。 - 循环 CAS: 由于多个线程可能同时进行
push
操作,CAS 操作可能会失败 (当其他线程先一步更新了head_
指针)。如果 CAS 操作失败,需要重新执行步骤 1-3,直到 CAS 操作成功。do-while
循环保证了循环 CAS 的执行,直到栈顶指针被成功更新。
③ pop
操作的无锁实现
pop
操作需要从栈顶移除一个节点,并返回节点的数据。无锁 pop
操作同样需要使用 CAS 操作原子性地更新 head_
指针。
1
std::optional<T> pop() {
2
Node* old_head = head_.load(std::memory_order_acquire); // 1. 读取当前栈顶指针
3
while (old_head != nullptr) { // 2. 栈不为空
4
Node* next_head = old_head->next; // 3. 获取新的栈顶指针 (当前栈顶的 next 指针)
5
if (head_.compare_exchange_weak(old_head, next_head, std::memory_order_acq_rel, std::memory_order_acquire)) { // 4. CAS 操作更新栈顶指针
6
T data = old_head->data;
7
delete old_head; // 5. 释放弹出的节点内存
8
return data; // 6. 返回弹出的数据
9
}
10
// CAS 失败,old_head 已被其他线程修改,重新读取最新的栈顶指针
11
old_head = head_.load(std::memory_order_acquire);
12
}
13
return std::nullopt; // 栈为空,返回 nullopt
14
}
pop
操作的步骤:
- 读取当前栈顶指针
old_head
: 使用head_.load(std::memory_order_acquire)
原子性地读取当前的栈顶指针。这里使用acquire
顺序,保证在pop
操作之后的所有操作,都发生在其他线程完成push
操作之前。 - 检查栈是否为空: 如果
old_head
为nullptr
,表示栈为空,直接返回std::nullopt
。 - 获取新的栈顶指针
next_head
: 如果栈不为空,获取当前栈顶节点old_head
的next
指针next_head
,next_head
将成为新的栈顶指针。 - CAS 操作更新栈顶指针
head_
: 使用head_.compare_exchange_weak(old_head, next_head, std::memory_order_acq_rel, std::memory_order_acquire)
原子性地尝试更新栈顶指针。
▮▮▮▮⚝old_head
(期望值): CAS 操作期望head_
的当前值等于old_head
。
▮▮▮▮⚝next_head
(新值): 如果 CAS 操作成功,将head_
的值更新为next_head
。
▮▮▮▮⚝std::memory_order_acq_rel
(成功时的内存顺序): 使用acq_rel
顺序,同时具有acquire
和release
顺序的特性,保证pop
操作的同步性。
▮▮▮▮⚝std::memory_order_acquire
(失败时的内存顺序): 使用acquire
顺序,失败时需要重新读取head_
指针,并保证读取操作的顺序性。 - 释放弹出的节点内存: 如果 CAS 操作成功,表示成功弹出一个节点,需要释放弹出的节点
old_head
的内存。 - 返回弹出的数据: 返回弹出的节点
old_head
的数据。 - 循环 CAS: 与
push
操作类似,pop
操作也需要循环 CAS,直到栈顶指针被成功更新。
④ empty
操作的无锁实现
empty
操作只需要简单地读取栈顶指针 head_
是否为空。
1
bool empty() const {
2
return head_.load(std::memory_order_relaxed) == nullptr; // 原子性地读取栈顶指针
3
}
empty
操作只需要读取 head_
指针,不需要修改任何状态,因此可以使用 relaxed
顺序,性能最高。
⑤ 无锁数据结构的优势和局限性
优势:
⚝ 避免锁竞争: 无锁数据结构不使用互斥锁,避免了锁竞争的开销,在高并发场景下可以提供更高的性能。
⚝ 无死锁: 无锁数据结构不会发生死锁,因为没有锁的存在。
⚝ 更高的可扩展性: 无锁数据结构通常具有更好的可扩展性,可以更好地利用多核处理器的性能。
局限性:
⚝ 实现复杂: 无锁数据结构的实现通常比基于互斥锁的数据结构复杂得多,需要深入理解原子操作和无锁算法。
⚝ 调试困难: 无锁程序的调试通常比基于互斥锁的程序更困难,因为数据竞争和并发错误更难以追踪和复现。
⚝ 并非所有问题都适合无锁: 无锁编程并非万能的,某些问题可能更适合使用基于互斥锁的同步机制。选择无锁还是有锁方案需要根据具体的应用场景和需求进行权衡。
⚝ ABA 问题: 某些无锁算法可能会遇到 ABA 问题,需要使用更高级的技术 (例如 hazard pointers, RCU) 来解决。
⑥ 总结
无锁数据结构是一种高级的并发编程技术,可以避免锁竞争的开销,提高并发程序的性能和可扩展性。无锁栈是一个简单的无锁数据结构示例,演示了如何使用原子操作实现无锁的 push
和 pop
操作。无锁数据结构的实现通常比较复杂,需要深入理解原子操作和无锁算法,并仔细权衡其优势和局限性,选择合适的并发编程方案。
7. 性能优化与调试技巧 (Performance Optimization and Debugging Techniques)
本章介绍使用 Folly 库进行性能优化和调试的技巧,包括性能分析工具、内存管理优化和常见的调试方法,帮助读者提升 Folly 应用的性能和稳定性。
7.1 性能分析工具与方法 (Performance Analysis Tools and Methods)
性能分析 (Performance Analysis) 是软件开发中至关重要的一环,尤其是在构建高性能的 Folly 应用时。了解如何有效地分析程序的性能瓶颈,可以帮助开发者有针对性地进行优化,提升程序运行效率。本节将介绍几种常用的性能分析工具和方法,帮助读者定位 Folly 应用的性能瓶颈。
7.1.1 使用 gprof 进行性能分析 (Performance Analysis with gprof)
gprof (GNU profiler) 是一款经典的性能剖析 (Profiling) 工具,它可以帮助开发者了解程序运行时函数的调用次数、执行时间以及函数间的调用关系。通过 gprof,我们可以识别出程序中的 CPU 密集型 (CPU-bound) 函数,即热点代码 (Hotspot Code),从而有针对性地进行优化。
① gprof 的工作原理
gprof 的工作原理主要基于采样 (Sampling) 和插桩 (Instrumentation) 技术。
▮▮▮▮ⓐ 插桩:在编译和链接阶段,gprof 会在程序的每个函数入口和出口处插入额外的代码,用于记录函数调用信息。
▮▮▮▮ⓑ 采样:在程序运行时,gprof 通过定期采样程序计数器 (Program Counter, PC) 的值,来统计程序在各个函数中花费的时间比例。采样频率通常由系统配置决定。
▮▮▮▮ⓒ 数据分析:程序运行结束后,gprof 会根据插桩和采样数据生成性能报告,包括函数调用图 (Call Graph)、函数占用时间百分比等信息。
② gprof 的使用步骤
使用 gprof 进行性能分析通常包括以下步骤:
① 编译时添加 -pg
选项:在编译 Folly 项目时,需要在编译和链接命令中添加 -pg
选项,以启用 gprof 的插桩功能。例如,对于 CMake 项目,可以在 CMakeLists.txt
中添加编译选项:
1
CMAKE_MINIMUM_REQUIRED(VERSION 3.15)
2
project(folly_example)
3
4
set(CMAKE_CXX_STANDARD 17)
5
6
# 添加 gprof 编译选项
7
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg")
8
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg")
9
10
find_package(Folly REQUIRED)
11
12
add_executable(example example.cpp)
13
target_link_libraries(example PRIVATE folly)
② 运行程序:编译生成可执行文件后,正常运行程序。程序结束后,会在当前目录下生成 gmon.out
文件,该文件包含了 gprof 采集的性能数据。
1
./example
③ 生成性能报告:使用 gprof
命令分析 gmon.out
文件,生成可读的性能报告。
1
gprof ./example gmon.out > profile.txt
④ 分析性能报告:打开 profile.txt
文件,分析性能报告。gprof 报告通常包含以下几个部分:
▮▮▮▮ⓐ Flat Profile (扁平 профиль):显示每个函数占用的 CPU 时间百分比、调用次数、每次调用平均时间等信息。这部分可以帮助快速找到 CPU 占用率最高的函数。
1
Flat profile:
2
3
Each sample counts as 0.01 seconds.
4
% cumulative self self total
5
time seconds seconds calls ms/call ms/call name
6
33.34 0.02 0.02 1 20.00 20.00 folly::fib(int)
7
33.33 0.04 0.02 2 10.00 10.00 folly::fib_recursive(int)
8
33.33 0.06 0.02 1 20.00 20.00 main
9
0.00 0.06 0.00 1 0.00 0.00 __libc_csu_fini
10
0.00 0.06 0.00 2 0.00 0.00 __libc_csu_init
11
0.00 0.06 0.00 1 0.00 0.00 _start
12
0.00 0.06 0.00 1 0.00 0.00 deregister_tm_clones
13
0.00 0.06 0.00 1 0.00 0.00 frame_dummy
14
0.00 0.06 0.00 1 0.00 0.00 register_tm_clones
15
0.00 0.06 0.00 1000 0.00 0.00 std::allocator<int>::allocate(unsigned long)
16
0.00 0.06 0.00 1000 0.00 0.00 std::allocator<int>::deallocate(int*, unsigned long)
17
0.00 0.06 0.00 1 0.00 0.00 std::ios_base::Init::Init()
18
0.00 0.06 0.00 1 0.00 0.00 std::ios_base::Init::~Init()
▮▮▮▮ⓑ Call Graph (调用图):显示函数之间的调用关系,以及每个函数及其子函数占用的 CPU 时间百分比。这部分可以帮助理解函数调用链,找出性能瓶颈的传播路径。
1
графа вызовов (включая время самозатрат функции)
2
3
index % time self children called name
4
self+children parents name
5
[1] 66.7 0.02 0.02 1/1 main [1]
6
0.02 0.02 1+2 folly::fib(int) [2]
7
-----------------------------------------------
8
0.02 0.02 1/2 folly::fib_recursive(int) [3]
9
[2] 66.7 0.02 0.02 1 folly::fib(int) [2]
10
0.02 0.02 1/1 main [1]
11
-----------------------------------------------
12
0.02 0.02 2/2 folly::fib_recursive(int) [3]
13
[3] 33.3 0.02 0.00 2 folly::fib_recursive(int) [3]
14
0.02 0.00 2/2 folly::fib(int) [2]
15
-----------------------------------------------
16
[4] 0.0 0.00 0.00 1 deregister_tm_clones [4]
17
[5] 0.0 0.00 0.00 1 frame_dummy [5]
18
[6] 0.0 0.00 0.00 1 register_tm_clones [6]
19
[7] 0.0 0.00 0.00 1 std::ios_base::Init::Init() [7]
20
[8] 0.0 0.00 0.00 1 std::ios_base::Init::~Init() [8]
21
0.00 0.00 1/1 __libc_csu_fini [9]
22
-----------------------------------------------
23
[9] 0.0 0.00 0.00 1 __libc_csu_fini [9]
24
0.00 0.00 1/1 _start [10]
25
0.00 0.00 1/1 std::ios_base::Init::~Init() [8]
26
-----------------------------------------------
27
[10] 0.0 0.00 0.00 1 _start [10]
28
0.00 0.00 1/1 register_tm_clones [6]
29
0.00 0.00 1/1 __libc_csu_init [11]
30
0.00 0.00 1/1 frame_dummy [5]
31
0.00 0.00 1/1 main [1]
32
0.00 0.00 1/1 std::ios_base::Init::Init() [7]
33
-----------------------------------------------
34
[11] 0.0 0.00 0.00 1 __libc_csu_init [11]
35
0.00 0.00 1/1 _start [10]
36
-----------------------------------------------
③ gprof 的优缺点
▮▮▮▮ⓐ 优点:
⚝ 简单易用:gprof 的使用方法相对简单,只需要添加编译选项和运行命令即可生成性能报告。
⚝ 函数级剖析:gprof 可以提供函数级别的性能数据,帮助开发者定位到具体的性能瓶颈函数。
⚝ 调用图分析:gprof 的调用图可以展示函数之间的调用关系,有助于理解程序执行流程和性能瓶颈的传播。
▮▮▮▮ⓑ 缺点:
⚝ 采样精度有限:gprof 基于采样技术,采样频率有限,可能无法精确捕捉到短暂的性能波动。
⚝ 运行时开销:插桩和采样会引入一定的运行时开销,可能影响程序的真实性能。
⚝ 线程支持较弱:对于多线程程序,gprof 的支持相对较弱,可能无法准确分析线程间的性能问题。
⚝ 不适用于所有场景:对于 I/O 密集型 (I/O-bound) 程序,gprof 的 CPU 采样可能无法有效地反映 I/O 操作的性能瓶颈。
尽管 gprof 存在一些局限性,但作为一款经典的性能剖析工具,它仍然可以在很多场景下帮助开发者快速定位 Folly 应用的 CPU 性能瓶颈。对于更复杂的性能分析需求,可以考虑使用更高级的工具,如 perf 或火焰图。
7.1.2 使用 perf 进行系统级性能分析 (System-Level Performance Analysis with perf)
perf (Performance Events) 是 Linux 内核自带的性能分析工具,它基于硬件性能计数器 (Hardware Performance Counters) 和内核 tracepoints,可以提供更底层、更全面的系统级性能数据。perf 不仅可以分析 CPU 性能,还可以分析内存访问、缓存命中率、I/O 操作等系统级别的性能指标。
① perf 的工作原理
perf 的工作原理主要基于硬件性能计数器和内核事件。
▮▮▮▮ⓐ 硬件性能计数器:现代 CPU 包含多种硬件性能计数器,用于记录 CPU 执行过程中的各种事件,如指令执行数、缓存访问次数、分支预测失败次数等。perf 可以读取这些硬件计数器的值,从而获取 CPU 的硬件性能数据。
▮▮▮▮ⓑ 内核事件 (Kernel Events):Linux 内核提供了丰富的 tracepoints 和 probes,用于跟踪内核的各种事件,如系统调用、进程调度、中断处理等。perf 可以利用这些内核事件,获取系统级的性能数据。
▮▮▮▮ⓒ 事件采样与分析:perf 可以配置不同的事件类型和采样频率,定期采样硬件性能计数器或内核事件,并将采样数据记录到文件中。然后,perf 可以对采样数据进行分析,生成各种性能报告,如火焰图、函数调用统计、热点路径分析等。
② perf 的使用步骤
使用 perf 进行性能分析通常包括以下步骤:
① 安装 perf 工具:如果系统没有预装 perf 工具,需要先安装。
1
sudo apt-get update
2
sudo apt-get install linux-perf
② 运行 perf record 命令:使用 perf record
命令运行 Folly 程序,并指定需要采样的事件类型和采样频率。例如,采样 CPU cycles 事件,采样频率为 99Hz:
1
perf record -F 99 -g -e cycles ./example
▮▮▮▮⚝ -F 99
:指定采样频率为 99Hz,即每秒采样 99 次。
▮▮▮▮⚝ -g
:启用调用图 (Call Graph) 记录,用于生成火焰图。
▮▮▮▮⚝ -e cycles
:指定采样的事件类型为 CPU cycles,即 CPU 周期数。可以使用 perf list
命令查看 perf 支持的所有事件类型。
▮▮▮▮⚝ ./example
:要分析的 Folly 程序。
③ 生成性能报告:程序运行结束后,perf 会在当前目录下生成 perf.data
文件,该文件包含了 perf 采集的性能数据。可以使用 perf report
命令生成文本形式的性能报告:
1
perf report
perf report
命令会打开一个交互式的界面,可以浏览性能报告,查看热点函数、调用图等信息。
④ 生成火焰图:可以使用 perf script
命令将 perf.data
文件转换为文本形式的事件记录,然后使用火焰图工具 (如 FlameGraph) 生成火焰图。
1
perf script > perf.unfold
2
./FlameGraph/stackcollapse-perf.pl perf.unfold > out.folded
3
./FlameGraph/flamegraph.pl out.folded > flamegraph.svg
▮▮▮▮⚝ perf script > perf.unfold
:将 perf.data
转换为文本形式的事件记录。
▮▮▮▮⚝ ./FlameGraph/stackcollapse-perf.pl perf.unfold > out.folded
:使用 stackcollapse-perf.pl
脚本将事件记录转换为火焰图工具可识别的 folded stacks 格式。
▮▮▮▮⚝ ./FlameGraph/flamegraph.pl out.folded > flamegraph.svg
:使用 flamegraph.pl
脚本生成火焰图 SVG 文件。
⑤ 分析性能报告和火焰图:分析 perf report
生成的文本报告和 flamegraph.svg
火焰图,定位性能瓶颈。perf 报告和火焰图可以提供更详细、更底层的性能信息,例如:
▮▮▮▮ⓐ CPU 使用率:了解程序在 CPU 上花费的时间比例。
▮▮▮▮ⓑ 指令执行数:统计程序执行的指令总数,可以反映代码的执行效率。
▮▮▮▮ⓒ 缓存命中率:分析程序的数据和指令缓存命中率,了解缓存使用效率。
▮▮▮▮ⓓ 分支预测:分析分支预测的准确率,了解分支预测对性能的影响。
▮▮▮▮ⓔ 系统调用:统计程序执行的系统调用次数和耗时,了解系统调用对性能的影响。
▮▮▮▮ⓕ 锁竞争:分析锁竞争情况,了解多线程程序的同步开销。
③ perf 的优缺点
▮▮▮▮ⓐ 优点:
⚝ 系统级剖析:perf 可以提供系统级的性能数据,包括 CPU 硬件性能计数器和内核事件,可以更全面地了解程序的性能瓶颈。
⚝ 低开销:perf 基于硬件性能计数器和内核事件,运行时开销相对较低,对程序性能的影响较小。
⚝ 多功能:perf 提供了丰富的子命令和选项,可以进行多种类型的性能分析,如 CPU 剖析、内存剖析、I/O 剖析等。
⚝ 火焰图支持:perf 可以生成火焰图,直观地展示函数调用关系和性能瓶颈。
▮▮▮▮ⓑ 缺点:
⚝ 使用复杂:perf 的使用方法相对复杂,需要理解各种事件类型和选项的含义。
⚝ 权限限制:perf 需要 root 权限才能访问硬件性能计数器和内核事件。
⚝ 数据解读:perf 生成的性能数据比较底层,需要一定的系统知识才能正确解读。
perf 是一款强大的系统级性能分析工具,可以帮助开发者深入了解 Folly 应用的性能瓶颈。对于需要精细化性能优化和系统级性能分析的场景,perf 是一个非常有力的工具。
7.1.3 火焰图 (Flame Graphs) 的生成与分析 (Generating and Analyzing Flame Graphs)
火焰图 (Flame Graph) 是一种可视化性能剖析结果的图表,它可以直观地展示程序在不同函数上的 CPU 时间分配情况。火焰图以图形化的方式呈现函数调用栈 (Call Stack) 的信息,帮助开发者快速定位 CPU 占用率高的代码路径。
① 火焰图的结构
火焰图由一系列堆叠的长方形条组成,每个长方形条代表一个函数在调用栈中的一个层级。
▮▮▮▮ⓐ X 轴 (横轴):表示时间,但火焰图并不精确表示时间流逝,而是表示采样次数或 CPU 时间的比例。X 轴上的宽度越大,表示该函数及其子函数占用的 CPU 时间比例越高。
▮▮▮▮ⓑ Y 轴 (纵轴):表示调用栈的深度。从下往上,每一层代表调用栈中的一个函数。最底层是程序入口函数 (如 main
函数),往上依次是被调用的函数。
▮▮▮▮ⓒ 颜色:火焰图通常使用暖色调 (如红色、橙色) 表示 CPU 占用率高的函数,冷色调 (如蓝色、绿色) 表示 CPU 占用率低的函数。颜色本身没有实际意义,只是为了区分不同的函数。
▮▮▮▮ⓓ 函数名称:每个长方形条上会显示函数的名称。
② 火焰图的生成方法
生成火焰图通常需要以下步骤:
① 性能数据采集:使用性能剖析工具 (如 perf, gprof, SystemTap 等) 采集程序的性能数据,包括函数调用栈信息和 CPU 时间采样数据。
② 数据转换:将采集的性能数据转换为火焰图工具 (如 FlameGraph) 可识别的 folded stacks 格式。folded stacks 是一种文本格式,每一行表示一个调用栈,以及该调用栈的采样次数或 CPU 时间。例如:
1
main;folly::fib;folly::fib_recursive 10
2
main;folly::fib;folly::fib_recursive 8
3
main;folly::fib 12
4
...
③ 生成火焰图:使用火焰图工具 (如 FlameGraph) 解析 folded stacks 格式的数据,生成火焰图 SVG 文件。
使用 perf 生成火焰图的步骤 (如 7.1.2 节所述):
1
perf record -F 99 -g -e cycles ./example
2
perf script > perf.unfold
3
./FlameGraph/stackcollapse-perf.pl perf.unfold > out.folded
4
./FlameGraph/flamegraph.pl out.folded > flamegraph.svg
③ 火焰图的分析方法
分析火焰图的关键是找到宽而高的 "火焰"。
▮▮▮▮ⓐ 宽度:火焰越宽,表示该函数及其子函数占用的 CPU 时间比例越高,可能是性能瓶颈。
▮▮▮▮ⓑ 高度:火焰的高度表示调用栈的深度。较高的火焰可能表示复杂的函数调用关系或深层嵌套调用。
▮▮▮▮ⓒ 顶端函数:火焰图最顶端的函数是 CPU 占用率最高的函数。通常需要关注火焰图顶端较宽的函数,这些函数可能是性能优化的重点。
▮▮▮▮ⓓ 调用路径:沿着火焰图从上往下追踪调用路径,可以了解性能瓶颈的传播路径。
分析示例:
假设生成的火焰图显示一个较宽的 "火焰" 顶端函数是 folly::fbstring::append
,这意味着字符串拼接操作占用了大量的 CPU 时间。开发者可以考虑优化字符串拼接操作,例如使用 folly::fbstring::reserve
预分配字符串空间,或者使用 folly::StringPiece
避免不必要的字符串拷贝。
④ 火焰图的优点
▮▮▮▮ⓐ 直观可视化:火焰图以图形化的方式展示性能数据,比文本报告更直观、易于理解。
▮▮▮▮ⓑ 快速定位瓶颈:通过观察火焰图的 "火焰",可以快速定位 CPU 占用率高的代码路径,找出性能瓶颈。
▮▮▮▮ⓒ 调用栈分析:火焰图可以展示函数调用栈信息,帮助理解函数调用关系和性能瓶颈的传播。
▮▮▮▮ⓓ 多工具支持:火焰图可以与多种性能剖析工具 (如 perf, gprof, SystemTap 等) 结合使用。
火焰图是一种强大的性能可视化工具,可以帮助开发者快速、直观地分析 Folly 应用的 CPU 性能瓶颈,并指导性能优化工作。
7.2 内存管理优化技巧 (Memory Management Optimization Techniques)
内存管理 (Memory Management) 是高性能 C++ 应用开发中的重要方面。高效的内存管理可以减少内存分配和释放的开销,降低内存碎片 (Memory Fragmentation),提升程序性能和稳定性。Folly 库提供了一些内存管理优化工具和技巧,本节将介绍如何使用 folly::MemoryPool
、对象池 (Object Pools) 以及如何避免内存泄漏 (Memory Leak) 和碎片。
7.2.1 使用 folly::MemoryPool 优化内存分配 (Optimizing Memory Allocation with folly::MemoryPool)
folly::MemoryPool
是 Folly 库提供的一个内存池 (Memory Pool) 实现。内存池是一种预先分配一块大的内存区域,然后从中按需分配小块内存的技术。它可以有效地减少频繁分配和释放小对象时的内存分配开销,提高内存分配效率。
① 内存池的原理
内存池的核心思想是预分配和重用。
▮▮▮▮ⓐ 预分配:在程序启动或需要时,内存池会预先分配一块大的连续内存区域,作为内存池的 "池子"。
▮▮▮▮ⓑ 分配:当程序需要分配小块内存时,内存池会从预分配的 "池子" 中划分出一块空闲内存块,分配给程序使用。
▮▮▮▮ⓒ 释放:当程序不再需要使用已分配的内存块时,内存池会将该内存块标记为 "空闲",放回 "池子" 中,以便后续重用,而不是立即释放给操作系统。
▮▮▮▮ⓓ 重用:当程序再次需要分配小块内存时,内存池会优先从 "池子" 中查找空闲的内存块进行分配,避免了向操作系统频繁申请内存的开销。
② folly::MemoryPool 的使用方法
folly::MemoryPool
提供了简单的 API,可以方便地创建和使用内存池。
① 创建 MemoryPool 对象:可以使用 folly::MemoryPool
的构造函数创建一个内存池对象。可以指定内存池的初始大小和增长策略。
1
#include <folly/MemoryPool.h>
2
3
folly::MemoryPool mp; // 创建默认配置的内存池
4
folly::MemoryPool mp_custom(1024 * 1024); // 创建初始大小为 1MB 的内存池
② 从内存池分配内存:使用 allocate()
方法从内存池中分配指定大小的内存块。allocate()
方法返回 void*
指针,需要手动转换为需要的类型指针。
1
void* ptr = mp.allocate(sizeof(int));
2
int* int_ptr = static_cast<int*>(ptr);
③ 将内存块放回内存池:使用 free()
方法将不再使用的内存块放回内存池。
1
mp.free(ptr);
④ 使用 MemoryPoolT 模板类:folly::MemoryPoolT<T>
是一个模板类,可以更方便地分配和释放特定类型的对象。MemoryPoolT<T>
内部封装了 folly::MemoryPool
,并提供了类型安全的 allocate()
和 free()
方法。
1
folly::MemoryPoolT<int> int_mp;
2
int* int_ptr = int_mp.allocate(); // 分配一个 int 类型的内存块
3
*int_ptr = 10;
4
int_mp.free(int_ptr); // 释放 int 类型的内存块
③ MemoryPool 的适用场景
folly::MemoryPool
适用于以下场景:
▮▮▮▮ⓐ 频繁分配和释放小对象:当程序需要频繁地分配和释放大量小对象时,使用内存池可以显著减少内存分配开销,提高性能。例如,网络编程中的消息对象、游戏开发中的游戏对象等。
▮▮▮▮ⓑ 固定大小的对象:内存池更适合分配固定大小的对象,因为内存池通常将预分配的内存块划分为固定大小的块。
▮▮▮▮ⓒ 性能敏感的应用:对于性能敏感的应用,内存分配的开销可能成为瓶颈。使用内存池可以有效地优化内存分配性能。
④ MemoryPool 的优势
▮▮▮▮ⓐ 减少内存分配开销:内存池通过预分配和重用内存,减少了向操作系统频繁申请内存的开销,提高了内存分配效率。
▮▮▮▮ⓑ 降低内存碎片:内存池可以更好地管理内存分配,减少内存碎片,提高内存利用率。
▮▮▮▮ⓒ 提高性能:在频繁分配和释放小对象的场景下,使用内存池可以显著提高程序性能。
⑤ 使用 MemoryPool 的注意事项
▮▮▮▮ⓐ 内存池大小:需要根据实际应用场景合理设置内存池的大小。如果内存池太小,可能无法满足内存分配需求,导致内存池频繁增长,反而降低性能。如果内存池太大,可能会浪费内存。
▮▮▮▮ⓑ 对象生命周期:使用内存池分配的对象,其生命周期需要由开发者手动管理。确保在对象不再使用时及时放回内存池,避免内存泄漏。
▮▮▮▮ⓒ 线程安全:folly::MemoryPool
本身不是线程安全的。在多线程环境下使用内存池,需要考虑线程安全问题,可以使用线程局部 (Thread-Local) 的内存池,或者使用锁保护内存池的访问。
folly::MemoryPool
是一个简单易用、高效的内存池实现,可以有效地优化 Folly 应用的内存分配性能。在需要频繁分配和释放小对象的场景下,可以考虑使用 folly::MemoryPool
。
7.2.2 对象池 (Object Pools) 的设计与实现 (Design and Implementation of Object Pools)
对象池 (Object Pool) 是一种创建和管理可重用对象的设计模式。与内存池类似,对象池也旨在减少对象的创建和销毁开销,提高性能。对象池维护着一组预先创建的对象实例,当程序需要使用对象时,从对象池中获取一个空闲对象,使用完毕后将对象放回对象池,而不是销毁对象。
① 对象池的原理
对象池的核心思想是对象重用。
▮▮▮▮ⓐ 预创建对象:在程序启动或需要时,对象池会预先创建一组对象实例,并将其放入对象池的 "池子" 中。
▮▮▮▮ⓑ 获取对象:当程序需要使用对象时,从对象池中获取一个空闲对象。如果对象池中没有空闲对象,可以根据策略 (如创建新对象、等待空闲对象等) 处理。
▮▮▮▮ⓒ 使用对象:程序使用从对象池中获取的对象,完成相应的操作。
▮▮▮▮ⓓ 放回对象:当程序不再需要使用对象时,将对象放回对象池,标记为 "空闲",以便后续重用,而不是销毁对象。
▮▮▮▮ⓔ 对象重用:当程序再次需要对象时,对象池会优先从 "池子" 中查找空闲对象进行分配,避免了频繁创建和销毁对象的开销。
② 对象池的设计与实现要点
设计和实现对象池需要考虑以下几个要点:
① 对象类型:对象池管理的对象类型。通常对象池管理的对象是创建和销毁开销较大的对象,或者需要频繁使用的对象。
② 池大小:对象池中预先创建的对象数量。池大小需要根据实际应用场景合理设置。池太小可能导致对象池耗尽,池太大可能会浪费内存。
③ 对象创建:对象池在初始化时如何创建对象实例。可以使用工厂模式 (Factory Pattern) 或直接构造对象。
④ 对象获取:对象池如何提供获取空闲对象的方法。可以使用栈 (Stack)、队列 (Queue) 或链表 (Linked List) 等数据结构来管理空闲对象。
⑤ 对象放回:程序使用完对象后如何将对象放回对象池。需要确保对象放回对象池后处于可用状态,可以被后续重用。
⑥ 对象重置:当对象从对象池中取出重用时,可能需要重置对象的状态,例如清空对象的数据或重置对象的成员变量。
⑦ 线程安全:在多线程环境下,对象池的访问需要考虑线程安全问题。可以使用锁 (Mutex) 或原子操作 (Atomic Operations) 等同步机制来保护对象池的访问。
③ 对象池的实现示例 (简易版)
下面是一个简易的对象池实现示例,使用 std::stack
管理空闲对象:
1
#include <stack>
2
#include <mutex>
3
4
template <typename T>
5
class ObjectPool {
6
public:
7
ObjectPool(size_t poolSize) {
8
for (size_t i = 0; i < poolSize; ++i) {
9
pool_.push(new T()); // 预创建对象并放入栈中
10
}
11
}
12
13
~ObjectPool() {
14
std::lock_guard<std::mutex> lock(mutex_);
15
while (!pool_.empty()) {
16
delete pool_.top();
17
pool_.pop();
18
}
19
}
20
21
T* acquire() {
22
std::lock_guard<std::mutex> lock(mutex_);
23
if (pool_.empty()) {
24
return new T(); // 池中没有空闲对象,创建新对象 (或抛出异常/等待)
25
}
26
T* obj = pool_.top();
27
pool_.pop();
28
return obj;
29
}
30
31
void release(T* obj) {
32
std::lock_guard<std::mutex> lock(mutex_);
33
pool_.push(obj); // 将对象放回栈中
34
}
35
36
private:
37
std::stack<T*> pool_; // 使用栈管理空闲对象
38
std::mutex mutex_; // 互斥锁,保护线程安全
39
};
使用示例:
1
#include <iostream>
2
3
class MyObject {
4
public:
5
MyObject() {
6
std::cout << "MyObject created" << std::endl;
7
}
8
~MyObject() {
9
std::cout << "MyObject destroyed" << std::endl;
10
}
11
12
void doSomething() {
13
std::cout << "MyObject doing something" << std::endl;
14
}
15
};
16
17
int main() {
18
ObjectPool<MyObject> pool(10); // 创建对象池,初始大小为 10
19
20
MyObject* obj1 = pool.acquire(); // 从对象池获取对象
21
obj1->doSomething();
22
pool.release(obj1); // 将对象放回对象池
23
24
MyObject* obj2 = pool.acquire(); // 再次从对象池获取对象 (重用)
25
obj2->doSomething();
26
pool.release(obj2);
27
28
return 0;
29
}
④ 对象池的适用场景
对象池适用于以下场景:
▮▮▮▮ⓐ 对象创建和销毁开销较大:当对象的构造函数和析构函数执行时间较长,或者对象创建和销毁操作比较频繁时,使用对象池可以减少对象创建和销毁的开销,提高性能。例如,数据库连接对象、线程对象、网络连接对象等。
▮▮▮▮ⓑ 对象状态可重置:对象的状态可以被重置,以便在对象放回对象池后可以被后续重用。
▮▮▮▮ⓒ 需要限制对象数量:对象池可以限制程序中同时存在的对象数量,避免资源耗尽。
⑤ 对象池的优势
▮▮▮▮ⓐ 减少对象创建和销毁开销:对象池通过对象重用,避免了频繁创建和销毁对象的开销,提高了性能。
▮▮▮▮ⓑ 提高性能:在对象创建和销毁开销较大的场景下,使用对象池可以显著提高程序性能。
▮▮▮▮ⓒ 资源管理:对象池可以管理对象资源,限制对象数量,避免资源泄漏或耗尽。
⑥ 使用对象池的注意事项
▮▮▮▮ⓐ 对象状态重置:使用对象池的对象,在放回对象池前需要确保对象状态被正确重置,以便后续重用。
▮▮▮▮ⓑ 对象生命周期:使用对象池的对象,其生命周期需要由开发者手动管理。确保在对象不再使用时及时放回对象池,避免资源泄漏或对象池耗尽。
▮▮▮▮ⓒ 线程安全:在多线程环境下使用对象池,需要考虑线程安全问题,可以使用锁或其他同步机制来保护对象池的访问。
▮▮▮▮ⓓ 池大小设置:需要根据实际应用场景合理设置对象池的大小。池太小可能导致对象池耗尽,池太大可能会浪费内存。
对象池是一种常用的设计模式,可以有效地优化 Folly 应用中对象的创建和销毁性能。在需要频繁使用开销较大的对象的场景下,可以考虑使用对象池。
7.2.3 内存泄漏检测与预防 (Memory Leak Detection and Prevention)
内存泄漏 (Memory Leak) 是指程序在动态分配内存后,由于某种原因未能及时释放已分配的内存,导致系统可用内存逐渐减少的现象。内存泄漏会导致程序性能下降、系统资源耗尽,甚至程序崩溃。在 Folly 应用开发中,预防和检测内存泄漏至关重要。
① 内存泄漏的常见原因
内存泄漏的常见原因包括:
▮▮▮▮ⓐ 忘记释放内存:程序动态分配内存后,忘记调用 delete
或 free
等函数释放内存。
1
void leakMemory() {
2
int* ptr = new int[100];
3
// ... 使用 ptr,但忘记 delete[] ptr;
4
}
▮▮▮▮ⓑ 异常安全问题:在动态分配内存后,如果程序在释放内存之前抛出异常,导致释放内存的代码没有执行到,就会发生内存泄漏。
1
void potentialLeak() {
2
int* ptr = new int[100];
3
try {
4
// ... 可能抛出异常的代码
5
if (/* condition */) {
6
throw std::runtime_error("Error occurred");
7
}
8
// ...
9
delete[] ptr; // 如果抛出异常,这行代码不会执行
10
} catch (...) {
11
// ... 异常处理
12
}
13
}
▮▮▮▮ⓒ 循环引用:在使用智能指针 (Smart Pointers) 时,如果出现循环引用 (Circular Reference),可能导致对象无法被正确释放,从而造成内存泄漏。
1
#include <memory>
2
3
struct Node {
4
std::shared_ptr<Node> next;
5
// ...
6
};
7
8
void circularReferenceLeak() {
9
std::shared_ptr<Node> node1 = std::make_shared<Node>();
10
std::shared_ptr<Node> node2 = std::make_shared<Node>();
11
node1->next = node2;
12
node2->next = node1; // 循环引用,node1 和 node2 无法被释放
13
}
② 内存泄漏检测工具
常用的内存泄漏检测工具包括:
▮▮▮▮ⓐ Valgrind:Valgrind 是一款强大的内存调试和性能分析工具,其中的 Memcheck 工具可以检测内存泄漏、内存越界访问、使用未初始化内存等内存错误。
▮▮▮▮ⓑ AddressSanitizer (ASan):AddressSanitizer 是一款快速的内存错误检测工具,可以检测内存泄漏、堆栈溢出、使用释放后内存等错误。ASan 由编译器 (如 GCC, Clang) 提供支持,需要在编译时添加 -fsanitize=address
选项。
▮▮▮▮ⓒ LeakSanitizer (LSan):LeakSanitizer 是 AddressSanitizer 的一部分,专门用于检测内存泄漏。LSan 可以在程序退出时报告内存泄漏信息。
使用 Valgrind 检测内存泄漏的步骤:
1
# 编译程序,无需特殊编译选项
2
g++ -o example example.cpp
3
4
# 使用 valgrind 运行程序,并检测内存泄漏
5
valgrind --leak-check=full ./example
Valgrind 会在程序退出时报告内存泄漏信息,包括泄漏内存的地址、大小、分配位置等。
使用 AddressSanitizer 和 LeakSanitizer 检测内存泄漏的步骤:
1
# 编译程序,添加 -fsanitize=address 编译和链接选项
2
g++ -fsanitize=address -o example example.cpp
3
4
# 运行程序
5
./example
如果程序存在内存泄漏,LeakSanitizer 会在程序退出时报告内存泄漏信息。
③ 内存泄漏预防技巧
预防内存泄漏的关键是良好的编程习惯和使用智能指针。
▮▮▮▮ⓐ 养成良好的内存管理习惯:
⚝ 配对使用 new
和 delete
(或 new[]
和 delete[]
):动态分配的内存必须配对使用 delete
或 delete[]
释放。
⚝ 及时释放不再使用的内存:在对象生命周期结束时,及时释放其占用的动态内存。
⚝ 避免在循环中分配内存而不释放:如果在循环中动态分配内存,务必在循环结束或适当的时候释放内存。
▮▮▮▮ⓑ 使用智能指针:智能指针 (如 std::unique_ptr
, std::shared_ptr
) 可以自动管理动态分配的内存,避免忘记释放内存导致的内存泄漏。
⚝ std::unique_ptr
:适用于独占所有权的场景,当 unique_ptr
对象销毁时,会自动释放其管理的内存。
1
#include <memory>
2
3
void useUniquePtr() {
4
std::unique_ptr<int[]> ptr(new int[100]); // 使用 unique_ptr 管理动态数组
5
// ... 使用 ptr,无需手动释放,unique_ptr 会自动释放
6
}
⚝ std::shared_ptr
:适用于共享所有权的场景,多个 shared_ptr
对象可以共享同一块内存的所有权。当最后一个 shared_ptr
对象销毁时,才会释放内存。需要注意避免循环引用导致的内存泄漏。可以使用 std::weak_ptr
打破循环引用。
▮▮▮▮ⓒ 使用 RAII (Resource Acquisition Is Initialization) 原则:RAII 是一种 C++ 编程技术,将资源的获取和释放与对象的生命周期绑定。通过 RAII,可以确保资源 (如内存、文件句柄、锁等) 在对象创建时获取,在对象销毁时自动释放,避免资源泄漏。智能指针就是 RAII 原则的典型应用。
▮▮▮▮ⓓ 代码审查和测试:定期进行代码审查 (Code Review),检查代码中是否存在内存泄漏的风险。编写单元测试 (Unit Test) 和集成测试 (Integration Test),使用内存泄漏检测工具 (如 Valgrind, ASan) 进行测试,尽早发现和修复内存泄漏问题。
内存泄漏是一个常见且严重的问题,可能导致 Folly 应用的性能和稳定性下降。通过使用内存泄漏检测工具和遵循内存泄漏预防技巧,可以有效地避免和解决内存泄漏问题,提高 Folly 应用的质量。
7.3 常见调试方法与工具 (Common Debugging Methods and Tools)
调试 (Debugging) 是软件开发过程中不可或缺的环节。当 Folly 应用出现错误或异常行为时,需要使用调试工具和方法来定位和解决问题。本节将介绍常用的调试工具 (如 gdb, lldb) 和 Folly 库提供的调试辅助工具和宏,帮助读者高效地调试 Folly 应用。
7.3.1 使用 gdb 进行程序调试 (Program Debugging with gdb)
gdb (GNU Debugger) 是一款强大的命令行调试器,可以用于调试 C、C++、Fortran 等多种编程语言编写的程序。gdb 提供了丰富的调试功能,如断点 (Breakpoint)、单步执行 (Step)、查看变量 (Variable)、查看调用栈 (Call Stack) 等,可以帮助开发者深入了解程序运行状态,定位错误原因。
① gdb 的常用命令
gdb 常用命令概览:
命令 | 描述 |
---|---|
break <location> | 在指定位置设置断点。<location> 可以是函数名、行号或文件名:行号。 |
run | 启动程序运行。 |
continue | 继续程序运行,直到下一个断点或程序结束。 |
next | 单步执行,跳过函数调用 (step over)。 |
step | 单步执行,进入函数调用 (step into)。 |
finish | 运行到当前函数结束并返回。 |
print <expression> | 打印表达式的值。<expression> 可以是变量、表达式或函数调用。 |
display <expression> | 持续显示表达式的值,每次程序停止时都会更新显示。 |
undisplay <display-number> | 取消显示指定编号的表达式。 |
backtrace (或 bt ) | 查看当前调用栈。 |
frame <frame-number> | 切换到指定编号的栈帧。 |
info locals | 查看当前栈帧的局部变量。 |
info args | 查看当前函数的参数。 |
list <location> | 列出源代码。<location> 可以是函数名、行号或文件名:行号。 |
quit (或 q ) | 退出 gdb。 |
help <command> | 查看指定命令的帮助信息。 |
② gdb 调试 Folly 应用的基本步骤
使用 gdb 调试 Folly 应用的基本步骤如下:
① 编译时添加 -g
选项:在编译 Folly 项目时,需要在编译和链接命令中添加 -g
选项,以生成调试信息。调试信息包含了源代码行号、变量名等信息,gdb 需要使用这些信息进行调试。例如,对于 CMake 项目,可以在 CMakeLists.txt
中添加编译选项:
1
CMAKE_MINIMUM_REQUIRED(VERSION 3.15)
2
project(folly_example)
3
4
set(CMAKE_CXX_STANDARD 17)
5
6
# 添加 gdb 调试选项
7
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
8
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -g")
9
10
find_package(Folly REQUIRED)
11
12
add_executable(example example.cpp)
13
target_link_libraries(example PRIVATE folly)
② 启动 gdb:使用 gdb
命令启动 gdb 调试器,并加载要调试的可执行文件。
1
gdb ./example
③ 设置断点:在程序中可能出错的位置设置断点。例如,在 main
函数入口处设置断点:
1
break main
或者在某个函数内部的特定行号设置断点:
1
break example.cpp:20
也可以在某个函数入口处设置断点:
1
break folly::fib
④ 运行程序:使用 run
命令启动程序运行。程序会运行到第一个断点处暂停。
1
run
⑤ 单步执行和查看变量:使用 next
(单步跳过)、step
(单步进入)、finish
(运行到函数结束) 等命令单步执行程序。使用 print <variable>
或 display <variable>
命令查看变量的值。
1
next
2
step
3
finish
4
print result
5
display i
⑥ 查看调用栈:使用 backtrace
(或 bt
) 命令查看当前调用栈,了解函数调用关系。使用 frame <frame-number>
命令切换到不同的栈帧,查看不同栈帧的局部变量和参数。
1
backtrace
2
frame 1
3
info locals
4
info args
⑦ 继续运行或退出:使用 continue
命令继续程序运行,直到下一个断点或程序结束。使用 quit
(或 q
) 命令退出 gdb 调试器。
1
continue
2
quit
③ gdb 调试技巧
▮▮▮▮ⓐ 条件断点 (Conditional Breakpoint):可以设置条件断点,只有当满足特定条件时,断点才会触发。例如,当变量 i
的值为 10 时触发断点:
1
break example.cpp:20 if i == 10
▮▮▮▮ⓑ 观察点 (Watchpoint):可以设置观察点,当某个变量的值发生变化时,程序会暂停执行。例如,当变量 result
的值发生变化时暂停:
1
watch result
▮▮▮▮ⓒ 函数断点 (Function Breakpoint):可以直接在函数名上设置断点,程序运行到函数入口处会暂停。
1
break folly::fib
▮▮▮▮ⓓ 使用 .gdbinit
文件:可以在用户主目录或项目根目录下创建 .gdbinit
文件,在文件中设置 gdb 的初始化命令,例如设置断点、设置显示格式等。gdb 启动时会自动加载 .gdbinit
文件中的命令。
gdb 是一款功能强大的调试器,可以帮助开发者深入了解 Folly 应用的运行状态,定位各种程序错误。熟练掌握 gdb 的常用命令和调试技巧,可以显著提高调试效率。
7.3.2 使用 lldb 进行程序调试 (Program Debugging with lldb)
lldb (LLVM Debugger) 是 LLVM 项目的默认调试器,也是 macOS 和 iOS 系统上的默认调试器。lldb 在功能上与 gdb 类似,但也具有一些自身的特点和优势,例如更好的 Python 脚本支持、更友好的用户界面、更好的模块化设计等。
① lldb 的常用命令
lldb 的命令语法与 gdb 类似,但也有一些差异。lldb 常用命令概览:
命令 | 描述 | gdb 对应命令 |
---|---|---|
breakpoint set --name <function> (或 b <function> ) | 在函数入口处设置断点。 | break <function> |
breakpoint set --line <line> (或 b <line> ) | 在指定行号设置断点。 | break <line> |
run (或 r ) | 启动程序运行。 | run |
continue (或 c ) | 继续程序运行。 | continue |
next (或 n ) | 单步执行,跳过函数调用 (step over)。 | next |
step (或 s ) | 单步执行,进入函数调用 (step into)。 | step |
thread step-out (或 finish ) | 运行到当前函数结束并返回。 | finish |
frame variable <variable> (或 v <variable> ) | 打印变量的值。 | print <variable> |
frame variable --show-location <variable> | 打印变量的值和内存地址。 | |
display <variable> | 持续显示变量的值。 | display <variable> |
undisplay <display-number> | 取消显示指定编号的变量。 | undisplay <display-number> |
thread backtrace (或 bt ) | 查看当前调用栈。 | backtrace |
frame select <frame-number> | 切换到指定编号的栈帧。 | frame <frame-number> |
frame info | 查看当前栈帧的信息。 | info frame |
help <command> (或 help ) | 查看命令帮助信息。 | help <command> |
quit (或 q ) | 退出 lldb。 | quit |
② lldb 调试 Folly 应用的基本步骤
lldb 调试 Folly 应用的基本步骤与 gdb 类似:
① 编译时添加 -g
选项:同样需要在编译和链接命令中添加 -g
选项,生成调试信息。
② 启动 lldb:使用 lldb
命令启动 lldb 调试器,并加载要调试的可执行文件。
1
lldb ./example
③ 设置断点:使用 breakpoint set
或 b
命令设置断点。例如,在 main
函数入口处设置断点:
1
breakpoint set --name main
或者在某个函数内部的特定行号设置断点:
1
breakpoint set --line 20 --file example.cpp
④ 运行程序:使用 run
或 r
命令启动程序运行。
1
run
⑤ 单步执行和查看变量:使用 next
(或 n
)、step
(或 s
)、thread step-out
(或 finish
) 等命令单步执行程序。使用 frame variable <variable>
(或 v <variable>
) 命令查看变量的值。
1
next
2
step
3
thread step-out
4
frame variable result
⑥ 查看调用栈:使用 thread backtrace
(或 bt
) 命令查看调用栈。使用 frame select <frame-number>
命令切换栈帧。
1
thread backtrace
2
frame select 1
3
frame info
⑦ 继续运行或退出:使用 continue
(或 c
) 命令继续程序运行。使用 quit
(或 q
) 命令退出 lldb 调试器。
③ lldb 的优势与特点
▮▮▮▮ⓐ 更好的 Python 脚本支持:lldb 提供了更强大的 Python 脚本支持,可以使用 Python 脚本扩展 lldb 的功能,自定义调试命令,自动化调试流程。
▮▮▮▮ⓑ 更友好的用户界面:lldb 的命令提示符更友好,支持命令自动补全、命令历史记录等功能,提高了调试效率。
▮▮▮▮ⓒ 更好的模块化设计:lldb 采用模块化设计,更容易扩展和定制。
▮▮▮▮ⓓ Clang/LLVM 集成:lldb 与 Clang/LLVM 编译器工具链紧密集成,可以更好地理解 C++ 代码,提供更准确的调试信息。
▮▮▮▮ⓔ 表达式求值:lldb 的表达式求值器 (expression evaluator) 更强大,可以支持更复杂的 C++ 表达式,包括 Lambda 表达式、模板、STL 容器等。
④ gdb vs lldb
gdb 和 lldb 都是优秀的 C++ 调试器,选择哪个调试器取决于个人偏好和使用场景。
特点 | gdb | lldb |
---|---|---|
历史 | 历史悠久,成熟稳定 | 后起之秀,发展迅速 |
平台 | 跨平台,支持多种操作系统 | 主要用于 macOS、iOS、Linux 等平台 |
Python 支持 | Python 脚本支持较弱 | Python 脚本支持强大,易于扩展和定制 |
用户界面 | 命令行界面相对简洁 | 命令行界面更友好,功能更丰富 |
C++ 支持 | C++ 支持较好 | C++ 支持更好,与 Clang/LLVM 集成 |
表达式求值 | 表达式求值器功能相对简单 | 表达式求值器功能强大,支持复杂 C++ 表达式 |
模块化 | 模块化设计较弱 | 模块化设计优秀,易于扩展 |
对于 Folly 应用开发,gdb 和 lldb 都是可行的调试工具。如果开发者更喜欢使用 Python 脚本扩展调试功能,或者在 macOS 或 iOS 系统上开发,lldb 可能是更好的选择。如果开发者更习惯使用传统的 gdb 命令,或者需要在更多平台上进行调试,gdb 仍然是一个可靠的选择。
7.3.3 Folly 提供的调试辅助工具与宏 (Debugging Utilities and Macros Provided by Folly)
Folly 库为了方便开发者进行调试,提供了一些调试辅助工具和宏,可以帮助开发者在代码中添加调试信息、进行断言检查、记录日志等。
① Folly 调试宏
Folly 提供了一些常用的调试宏,定义在 <folly/Debug.h>
头文件中。
▮▮▮▮ⓐ CHECK(condition)
:断言宏,用于检查条件是否为真。如果条件为假 (false),CHECK
宏会打印错误信息并终止程序执行。在 Release 版本中,CHECK
宏会被禁用 (编译为空语句),不会影响性能。
1
#include <folly/Debug.h>
2
3
void testCheck(int value) {
4
CHECK(value >= 0); // 断言 value 必须大于等于 0
5
// ...
6
}
▮▮▮▮ⓑ CHECK_EQ(val1, val2)
, CHECK_NE(val1, val2)
, CHECK_LT(val1, val2)
, CHECK_LE(val1, val2)
, CHECK_GT(val1, val2)
, CHECK_GE(val1, val2)
:比较断言宏,用于检查两个值之间的比较关系。如果比较关系不成立,宏会打印错误信息并终止程序执行。例如,检查 a
是否等于 b
:
1
CHECK_EQ(a, b);
▮▮▮▮ⓒ LOG(level) << message
:日志宏,用于记录日志信息。level
参数指定日志级别,可以是 INFO
, WARN
, ERROR
, FATAL
等。message
参数是要记录的日志消息。Folly 的日志系统可以配置日志输出目的地 (如控制台、文件、syslog 等) 和日志级别。
1
#include <folly/logging/xlog.h>
2
3
void testLog(int value) {
4
XLOG(INFO) << "Function testLog called with value: " << value;
5
if (value < 0) {
6
XLOG(WARN) << "Value is negative: " << value;
7
}
8
// ...
9
}
▮▮▮▮ⓓ VLOG(vlevel) << message
:VLOG (Verbose Log) 宏,用于记录更详细的日志信息。vlevel
参数指定详细日志级别,取值范围为 0 到 3。VLOG 宏只有在设置了相应的详细日志级别时才会输出日志信息。
1
#include <folly/logging/xlog.h>
2
3
void testVLog(int value) {
4
VLOG(1) << "Detailed log: value = " << value; // 详细日志级别为 1
5
// ...
6
}
▮▮▮▮ⓔ DCHECK(condition)
, DCHECK_EQ(...)
, DCHECK_NE(...)
, ...:Debug 断言宏,与 CHECK
系列宏类似,但在 Release 版本中会被完全移除 (包括条件判断代码),不会有任何性能开销。适用于对性能要求极高的场景。
② Folly 调试工具
▮▮▮▮ⓐ folly::pretty_print
:folly::pretty_print
函数可以将 C++ 对象 (包括 STL 容器、Folly 容器等) 以易读的格式打印到输出流 (如 std::cout
)。可以方便地查看对象的内容,辅助调试。
1
#include <folly/PrettyPrint.h>
2
#include <vector>
3
4
int main() {
5
std::vector<int> vec = {1, 2, 3, 4, 5};
6
folly::pretty_print(std::cout, vec) << std::endl; // 打印 vector 的内容
7
// 输出: [1, 2, 3, 4, 5]
8
return 0;
9
}
▮▮▮▮ⓑ folly::demangle
:folly::demangle
函数可以将 C++ 编译器 mangled 的函数名或类型名 demangle 成可读的名称。在查看调用栈或日志信息时,可以使用 folly::demangle
将 mangled name 转换为易读的名称。
1
#include <folly/Demangle.h>
2
#include <iostream>
3
4
template <typename T>
5
void printTypeName() {
6
std::cout << "Type name: " << folly::demangle(typeid(T).name()) << std::endl;
7
}
8
9
int main() {
10
printTypeName<std::vector<int>>();
11
// 输出: Type name: std::vector<int, std::allocator<int> >
12
return 0;
13
}
▮▮▮▮ⓒ folly::exception_tracer
:folly::exception_tracer
可以记录异常 (Exception) 的详细信息,包括异常类型、错误消息、调用栈等。在程序抛出异常时,可以使用 folly::exception_tracer
记录异常信息,方便后续分析和调试。
③ 使用 Folly 调试工具和宏的建议
▮▮▮▮ⓐ 合理使用断言宏:在代码中使用 CHECK
或 DCHECK
宏进行断言检查,可以尽早发现程序中的逻辑错误。在 Debug 版本中启用断言检查,在 Release 版本中禁用断言检查,以减少性能开销。
▮▮▮▮ⓑ 使用日志宏记录调试信息:在代码中使用 LOG
或 VLOG
宏记录调试信息,可以帮助了解程序的运行状态。根据需要配置日志级别,输出不同详细程度的日志信息。
▮▮▮▮ⓒ 结合调试器和 Folly 调试工具:将 Folly 提供的调试工具和宏与 gdb 或 lldb 等调试器结合使用,可以更高效地进行 Folly 应用的调试。例如,在 gdb 中设置断点,在断点处使用 folly::pretty_print
打印对象内容,使用 folly::demangle
查看 demangled name 等。
Folly 提供的调试辅助工具和宏可以有效地提高 Folly 应用的调试效率。合理使用这些工具和宏,可以帮助开发者快速定位和解决问题,提升 Folly 应用的开发效率和质量。
8. Folly 库的高级主题与未来展望 (Advanced Topics and Future Prospects of Folly Library)
章节概要
本章探讨 Folly 库的高级主题,如与其他库的集成、扩展 Folly 功能以及 Folly 库的未来发展方向,为读者提供更广阔的视野。
8.1 Folly 与其他库的集成 (Integration of Folly with Other Libraries)
章节概要
介绍 Folly 库与其他常用 C++ 库的集成,如 Boost, gRPC, Thrift 等,以及如何在项目中组合使用它们。
8.1.1 Folly 与 Boost 的协同使用 (Synergistic Use of Folly and Boost)
小节概要
讲解如何在项目中同时使用 Folly 和 Boost 库,以及它们之间的互补性和兼容性。
① Boost 库的定位与价值 (Positioning and Value of Boost Library)
Boost 库是 C++ 社区中一个久负盛名的、经过同行评审的、广泛开源的 C++ 库集合。它旨在为 C++ 程序员提供高质量、高性能、可移植的库,以扩展 C++ 标准库 (STL) 的功能。Boost 涵盖了广泛的领域,包括:
▮ 字符串和文本处理 (Strings and Text Processing)
▮ 容器和数据结构 (Containers and Data Structures)
▮ 算法 (Algorithms)
▮ 数学 (Mathematics)
▮ 并发与多线程 (Concurrency and Multithreading)
▮ 智能指针 (Smart Pointers)
▮ 模板元编程 (Template Metaprogramming)
▮ 函数对象 (Function Objects)
▮ 日期和时间 (Date and Time)
▮ 文件系统 (Filesystem)
▮ 网络编程 (Network Programming)
▮ ... 以及更多
Boost 库在 C++ 生态系统中扮演着至关重要的角色,许多 Boost 库已经成为或正在成为 C++ 标准库的一部分。例如,std::shared_ptr
智能指针最初就来自于 Boost.SmartPtr 库。Boost 库的设计理念是前瞻性和实验性的,它经常作为新特性的试验田,为 C++ 标准的演进做出贡献。
② Folly 库与 Boost 库的关系 (Relationship between Folly and Boost Library)
Folly 库和 Boost 库都是优秀的 C++ 开源库,它们在某些方面有重叠,但在设计目标和侧重点上有所不同。理解它们之间的关系有助于开发者在项目中更好地选择和使用它们。
▮ 重叠领域 (Overlapping Areas):
Folly 和 Boost 在某些领域提供了相似的功能,例如:
▮▮▮▮⚝ 字符串处理 (String Processing):两者都提供了高效的字符串处理工具,例如 Folly 的 fbstring
和 Boost.StringAlgo。
▮▮▮▮⚝ 智能指针 (Smart Pointers):Folly 提供了 folly::SharedPtr
等智能指针,Boost.SmartPtr 更是智能指针的集大成者。
▮▮▮▮⚝ 异步编程 (Asynchronous Programming):Folly 的 Future
/Promise
和 Boost.Asio 都提供了异步编程的工具。
▮▮▮▮⚝ 容器 (Containers):两者都提供了一些标准库之外的容器,例如 Folly 的 FBVector
和 Boost.Container。
▮ 设计理念的差异 (Differences in Design Philosophy):
▮▮▮▮⚝ Folly:更侧重于高性能和实用性,是 Facebook 在高负载、大规模应用场景下长期实践的产物。Folly 库的设计目标是解决 Facebook 内部遇到的实际问题,因此更加注重效率和工程实践。
▮▮▮▮⚝ Boost:更侧重于通用性和前瞻性,目标是成为 C++ 标准库的扩展和试验场。Boost 库的设计目标是广泛适用,覆盖更多的应用场景,并且尝试新的技术和设计模式。
▮ 互补性 (Complementarity):
尽管存在重叠,Folly 和 Boost 在很多方面是互补的。
▮▮▮▮⚝ Boost 的广度和深度:Boost 库涵盖的领域更广泛,拥有更丰富的库组件,在某些领域(例如数学、图论、正则表达式)Boost 提供了更为成熟和全面的解决方案。
▮▮▮▮⚝ Folly 的性能优势:Folly 库在某些特定领域(例如字符串处理、哈希容器、异步编程)进行了深度优化,提供了比 Boost 或标准库更出色的性能。
③ 协同使用策略与示例 (Strategies and Examples of Synergistic Use)
在实际项目中,可以根据需求将 Folly 和 Boost 库结合使用,充分发挥各自的优势。以下是一些协同使用策略和示例:
▮ 基础设施层 (Infrastructure Layer):
在项目的底层基础设施层,可以优先考虑使用 Folly 库,以获得更好的性能。例如:
▮▮▮▮⚝ 使用 folly::fbstring
替代 std::string
或 boost::string
处理高性能字符串操作。
▮▮▮▮⚝ 使用 F14Map
或 F14Set
替代 std::unordered_map
或 boost::unordered_map
处理高性能哈希容器。
▮▮▮▮⚝ 使用 Folly 的 Future
/Promise
和 Executor
构建高性能异步框架。
▮▮▮▮⚝ 使用 Folly 的 EventBase
和 AsyncSocket
构建高性能网络服务。
▮ 应用逻辑层 (Application Logic Layer):
在项目的应用逻辑层,可以根据具体需求选择 Boost 或 Folly 库,或者两者结合使用。例如:
▮▮▮▮⚝ 对于复杂的日期时间处理,可以使用 boost::date_time
库,它提供了比 std::chrono
更丰富的功能。
▮▮▮▮⚝ 对于需要使用正则表达式的场景,可以使用 boost::regex
库,它是 C++ 标准库 std::regex
的一个成熟且功能强大的替代品。
▮▮▮▮⚝ 对于需要进行图算法分析的应用,可以使用 boost::graph
库,它提供了丰富的图数据结构和算法。
▮▮▮▮⚝ 在需要进行更高级的并发控制时,可以使用 boost::asio
中提供的协程或者其他并发工具,与 Folly 的异步框架协同工作。
▮ 示例代码 (Code Example):
1
#include <iostream>
2
#include <string>
3
#include <vector>
4
5
#include <folly/FBVector.h>
6
#include <folly/FBString.h>
7
#include <boost/algorithm/string.hpp>
8
#include <boost/date_time/posix_time/posix_time.hpp>
9
10
int main() {
11
// 使用 folly::fbstring
12
folly::fbstring fbStr = "hello folly";
13
std::cout << "Folly string: " << fbStr.toStdString() << std::endl;
14
15
// 使用 boost::algorithm 处理字符串
16
std::string boostStr = " BOOST string ";
17
boost::trim(boostStr); // trim in-place
18
std::cout << "Boost string after trim: " << boostStr << std::endl;
19
20
// 使用 folly::FBVector
21
folly::FBVector<int> fbVec = {1, 2, 3, 4, 5};
22
std::cout << "Folly vector size: " << fbVec.size() << std::endl;
23
24
// 使用 boost::posix_time 获取当前时间
25
boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
26
std::cout << "Current time (Boost.DateTime): " << now << std::endl;
27
28
return 0;
29
}
代码解释:
⚝ 这个示例程序展示了如何在同一个 C++ 程序中同时使用 Folly 和 Boost 库。
⚝ 使用了 folly::fbstring
进行字符串操作,并将其转换为 std::string
以便与 std::cout
兼容。
⚝ 使用了 boost::algorithm::trim
函数来去除 Boost 字符串的空格。
⚝ 使用了 folly::FBVector
创建了一个 Folly 向量容器。
⚝ 使用了 boost::posix_time
库来获取当前时间。
编译和链接:
要编译和运行这个程序,需要确保你的编译环境配置了 Folly 和 Boost 库。CMakeLists.txt 文件可能需要添加以下内容:
1
cmake_minimum_required(VERSION 3.10)
2
project(FollyBoostExample)
3
4
find_package(Folly REQUIRED)
5
find_package(Boost REQUIRED COMPONENTS date_time algorithm)
6
7
add_executable(example main.cpp)
8
target_link_libraries(example PRIVATE Folly Boost::date_time Boost::algorithm)
这个 CMakeLists.txt 文件会查找 Folly 和 Boost 库,并将它们链接到你的可执行文件。
④ 注意事项与最佳实践 (Precautions and Best Practices)
▮ 依赖管理 (Dependency Management):
同时使用 Folly 和 Boost 可能会增加项目的依赖复杂性。需要仔细管理依赖关系,避免版本冲突和不必要的依赖引入。建议使用包管理器 (如 Conan
, vcpkg
) 或者 CMake 的 FetchContent
模块来管理依赖。
▮ 命名冲突 (Naming Conflicts):
虽然 Folly 和 Boost 都使用了命名空间来避免全局命名冲突,但在某些情况下仍然可能存在命名相似性导致的问题。在代码中明确指定命名空间 (例如 folly::fbstring
, boost::algorithm::trim
) 可以减少命名冲突的风险。
▮ 编译时间 (Compilation Time):
Folly 和 Boost 都是大型库,同时使用可能会增加编译时间。合理地组织代码结构,使用预编译头文件 (precompiled headers) 等技术可以缓解编译时间过长的问题。
▮ 学习曲线 (Learning Curve):
掌握 Folly 和 Boost 需要一定的学习成本。团队成员需要投入时间学习这两个库的特性和用法。
总而言之,Folly 和 Boost 都是 C++ 生态系统中非常重要的库。它们之间不是竞争关系,而是互补关系。在项目中合理地结合使用 Folly 和 Boost,可以充分利用它们的优势,构建更高效、更健壮的 C++ 应用。
8.1.2 Folly 与 gRPC 的集成 (Integration of Folly and gRPC)
小节概要
介绍如何将 Folly 库与 gRPC 框架集成,构建高性能的 RPC 服务。
① gRPC 框架简介 (Introduction to gRPC Framework)
gRPC 是一个高性能、开源、通用的 RPC (Remote Procedure Call, 远程过程调用) 框架,最初由 Google 开发。gRPC 使用 Protocol Buffers 作为接口定义语言 (IDL) 和消息序列化格式,支持多种编程语言,并基于 HTTP/2 协议进行传输。gRPC 的主要特点包括:
▮ 高性能 (High Performance):
▮▮▮▮⚝ Protocol Buffers:高效的序列化和反序列化。
▮▮▮▮⚝ HTTP/2:多路复用、头部压缩、服务器推送等特性提升传输效率。
▮▮▮▮⚝ 异步通信:支持异步 API,充分利用服务器资源。
▮ 强大的 IDL (Strong IDL):
▮▮▮▮⚝ Protocol Buffers:清晰地定义服务接口和数据结构,支持代码自动生成。
▮▮▮▮⚝ 跨语言兼容:支持多种编程语言 (C++, Java, Python, Go, etc.)。
▮ 多语言支持 (Multi-language Support):
gRPC 支持多种编程语言,使得构建跨语言的微服务架构变得容易。
▮ 流式处理 (Streaming):
支持客户端流式、服务器端流式和双向流式 RPC,适用于需要传输大量数据的场景。
▮ 认证与安全 (Authentication and Security):
内置多种认证机制 (如 SSL/TLS, OAuth 2.0),保障通信安全。
gRPC 广泛应用于构建微服务、移动应用后端、分布式系统等场景,特别适合对性能和效率有较高要求的应用。
② Folly 与 gRPC 集成的价值 (Value of Integrating Folly with gRPC)
虽然 gRPC 本身已经是一个高性能的 RPC 框架,但与 Folly 库集成可以进一步提升 gRPC 应用的性能、效率和可维护性。Folly 可以为 gRPC 提供以下增强:
▮ 更高效的异步编程 (More Efficient Asynchronous Programming):
▮▮▮▮⚝ Folly Futures 和 Executors:Folly 提供了强大而灵活的异步编程工具,可以替代 gRPC 默认的异步机制,提供更精细的控制和更高的性能。
▮▮▮▮⚝ 协程 (Coroutines):Folly 的协程支持可以简化 gRPC 异步代码的编写,提高代码可读性和可维护性。
▮ 优化的数据结构 (Optimized Data Structures):
▮▮▮▮⚝ fbstring
和 F14 容器:在 gRPC 服务中,字符串处理和数据存储是非常常见的操作。使用 Folly 的 fbstring
和 F14 容器可以显著提升性能,尤其是在高负载场景下。
▮ 强大的工具库 (Powerful Utility Library):
▮▮▮▮⚝ Folly 提供各种实用工具,例如时间处理、配置管理、调试工具等,可以简化 gRPC 应用的开发和维护。
▮▮▮▮⚝ EventBase:虽然 gRPC 已经有自己的事件循环机制,但在某些特定场景下,结合 Folly 的 EventBase
可以实现更灵活的事件处理。
③ 集成方式与示例 (Integration Methods and Examples)
Folly 与 gRPC 的集成主要体现在异步编程模型的融合和数据结构、工具库的应用。以下是一些集成方式和示例:
▮ 使用 Folly Futures 替代 gRPC Futures (Using Folly Futures instead of gRPC Futures):
gRPC 默认使用自身的 grpc::ClientReaderWriter
和回调机制进行异步操作。可以将其替换为 Folly 的 Future
/Promise
模型,以获得更强大的异步控制能力。
示例代码 (伪代码):
1
// 假设有一个 gRPC 服务接口定义
2
service MyService {
3
rpc MyMethod(MyRequest) returns (MyResponse);
4
}
5
6
// 使用 gRPC 生成的代码通常是这样的异步调用方式:
7
class MyServiceImpl final : public MyService::Service {
8
grpc::Status MyMethod(grpc::ServerContext* context, const MyRequest* request, MyResponse* reply) override {
9
// ... 业务逻辑 ...
10
return grpc::Status::OK;
11
}
12
};
13
14
// 集成 Folly Futures 的异步调用方式 (示例):
15
folly::Future<MyResponse> AsyncMyMethod(MyRequest request) {
16
folly::Promise<MyResponse> promise;
17
grpc::stub::async::MyMethod(
18
grpc::ClientContext(), request,
19
[&promise](grpc::Status status, const MyResponse& reply) {
20
if (status.ok()) {
21
promise.setValue(reply);
22
} else {
23
promise.setException(std::runtime_error(status.error_message()));
24
}
25
});
26
return promise.getFuture();
27
}
28
29
// 在 gRPC 服务端,可以使用 Folly Executors 处理请求:
30
class MyServiceImpl final : public MyService::Service {
31
folly::Executor* executor_; // 注入 Folly Executor
32
33
public:
34
MyServiceImpl(folly::Executor* executor) : executor_(executor) {}
35
36
grpc::Status MyMethod(grpc::ServerContext* context, const MyRequest* request, MyResponse* reply) override {
37
folly::via(executor_, [this, request, reply]() {
38
// 在 Folly Executor 上执行业务逻辑
39
// ... 业务逻辑,可以使用 Folly 的各种工具 ...
40
return folly::Unit(); // 返回 folly::Unit 表示 void
41
}).wait(); // 同步等待,或者使用 then/map 等异步组合
42
return grpc::Status::OK;
43
}
44
};
代码解释:
⚝ 示例代码展示了如何将 gRPC 的异步回调转换为 Folly 的 Future
。
⚝ 在客户端,AsyncMyMethod
函数返回一个 folly::Future<MyResponse>
,封装了 gRPC 异步调用的结果。
⚝ 在服务端,可以使用 folly::via
将 gRPC 请求处理逻辑调度到 Folly 的 Executor
上执行,充分利用 Folly 的线程池和异步调度能力。
▮ 使用 Folly 数据结构优化 gRPC 消息处理 (Using Folly Data Structures for gRPC Message Handling):
在 gRPC 消息处理过程中,可以使用 Folly 的 fbstring
替代 std::string
,使用 F14 容器替代 std::unordered_map
等标准容器,以提升性能。
示例代码 (伪代码):
1
#include <folly/FBString.h>
2
#include <folly/container/F14Map.h>
3
#include "my_grpc_proto.pb.h" // 假设生成的 gRPC Protobuf 头文件
4
5
class MyServiceImpl final : public MyService::Service {
6
grpc::Status MyMethod(grpc::ServerContext* context, const MyRequest* request, MyResponse* reply) override {
7
// 使用 folly::fbstring 处理字符串
8
folly::fbstring name = request->name();
9
folly::fbstring processedName = folly::toUpper(name);
10
reply->set_processed_name(processedName.toStdString());
11
12
// 使用 F14Map 存储数据
13
folly::F14Map<int, folly::fbstring> dataMap;
14
dataMap[1] = "value1";
15
dataMap[2] = "value2";
16
// ...
17
18
return grpc::Status::OK;
19
}
20
};
代码解释:
⚝ 在 gRPC 服务实现中,直接使用 folly::fbstring
来处理 Protobuf 消息中的字符串字段。
⚝ 使用 folly::F14Map
作为服务端内部数据存储容器。
④ 注意事项与最佳实践 (Precautions and Best Practices)
▮ 兼容性 (Compatibility):
确保 Folly 和 gRPC 的版本兼容。查看 Folly 和 gRPC 的官方文档,了解它们之间的兼容性信息。
▮ 构建配置 (Build Configuration):
正确配置 CMake 或其他构建系统,将 Folly 和 gRPC 链接到你的项目。
▮ 异步模型选择 (Asynchronous Model Choice):
根据项目需求选择合适的异步模型。Folly Futures 提供了更强大的控制能力,但可能需要更多的代码改动。
▮ 性能测试 (Performance Testing):
集成 Folly 后,进行充分的性能测试,验证性能提升效果,并找出潜在的性能瓶颈。
通过将 Folly 与 gRPC 集成,可以构建出更高效、更强大的 RPC 服务,特别是在需要处理高并发、低延迟请求的场景下,Folly 的性能优势可以得到充分发挥。
8.1.3 Folly 与 Thrift 的集成 (Integration of Folly and Thrift)
小节概要
介绍如何将 Folly 库与 Thrift 框架集成,构建跨语言的服务。
① Thrift 框架简介 (Introduction to Thrift Framework)
Apache Thrift 是一个跨语言的 RPC 框架,由 Facebook (Meta) 开源。与 gRPC 类似,Thrift 也使用接口定义语言 (IDL) 来定义服务接口和数据类型,并支持多种编程语言。Thrift 的主要特点包括:
▮ 跨语言 (Cross-language):
支持多种编程语言 (C++, Java, Python, PHP, Ruby, Erlang, Go, etc.),允许不同语言的服务进行互操作。
▮ 可扩展的传输和协议 (Extensible Transport and Protocol):
Thrift 支持多种传输协议 (如 TCP, HTTP, 内存) 和协议格式 (如二进制、压缩二进制、JSON),可以根据需求灵活选择。
▮ 代码生成 (Code Generation):
Thrift 编译器根据 IDL 文件生成各种语言的服务端和客户端代码,简化开发流程。
▮ 多种序列化方式 (Multiple Serialization Methods):
支持多种序列化协议,包括二进制协议、Compact 协议、JSON 协议等。
Thrift 最初由 Facebook 开发,并在 Facebook 内部广泛使用,后来开源成为 Apache 顶级项目。Thrift 适用于构建跨语言、需要灵活传输和协议选择的服务架构。
② Folly 与 Thrift 集成的价值 (Value of Integrating Folly with Thrift)
与 gRPC 类似,Thrift 本身也是一个强大的 RPC 框架。集成 Folly 库可以为 Thrift 应用带来以下优势:
▮ 高性能的 C++ 后端 (High-Performance C++ Backend):
▮▮▮▮⚝ Folly 优化:对于使用 C++ 作为后端的 Thrift 服务,集成 Folly 可以利用 Folly 的高性能组件,如 fbstring
, F14 容器, Future
/Promise
, Executor
, EventBase
等,提升服务性能和效率。
▮ 异步编程增强 (Asynchronous Programming Enhancement):
▮▮▮▮⚝ Folly 异步模型:Thrift C++ 库也支持异步编程,但集成 Folly 可以使用 Folly 成熟的异步框架,提供更统一、更强大的异步编程体验。
▮ 更好的工具和库支持 (Better Tools and Library Support):
▮▮▮▮⚝ Folly 工具库:Folly 提供的各种实用工具和库可以简化 Thrift C++ 服务的开发和维护,例如配置管理、日志记录、调试工具等。
③ 集成方式与示例 (Integration Methods and Examples)
Folly 与 Thrift 的集成主要集中在使用 Folly 组件替代或增强 Thrift C++ 库的默认实现。
▮ 使用 Folly 数据结构替代 Thrift 默认实现 (Using Folly Data Structures to Replace Thrift Defaults):
Thrift C++ 库默认使用 std::string
和 std::vector
等标准容器。可以替换为 Folly 的 fbstring
和 FBVector
等,以获得更好的性能。
示例代码 (伪代码):
1
// 假设有一个 Thrift 服务接口定义 (my_service.thrift)
2
// ...
3
service MyService {
4
string MyMethod(1: string request);
5
}
6
// ...
7
8
// 使用 Thrift 编译器生成 C++ 代码后,修改生成的代码
9
#include <thrift/server/TThreadedServer.h>
10
#include <thrift/transport/TServerSocket.h>
11
#include <thrift/transport/TBufferTransports.h>
12
#include <folly/FBString.h>
13
#include "MyService.h" // 假设生成的 Thrift 头文件
14
15
using namespace apache::thrift;
16
using namespace apache::thrift::server;
17
using namespace apache::thrift::transport;
18
using namespace apache::thrift::protocol;
19
20
class MyServiceHandler : virtual public MyServiceIf {
21
public:
22
MyServiceHandler() {}
23
24
void MyMethod(folly::fbstring& _return, const folly::fbstring& request) override {
25
// 使用 folly::fbstring 处理请求和返回结果
26
_return = folly::toUpper(request);
27
}
28
};
29
30
int main() {
31
// ... Thrift 服务端代码 ...
32
::std::shared_ptr<MyServiceHandler> handler(new MyServiceHandler());
33
::std::shared_ptr<TProcessor> processor(new MyServiceProcessor(handler));
34
::std::shared_ptr<TServerTransport> serverTransport(new TServerSocket(9090));
35
::std::shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
36
::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());
37
38
TThreadedServer server(processor, serverTransport, transportFactory, protocolFactory);
39
server.serve();
40
return 0;
41
}
代码解释:
⚝ 在 Thrift 服务接口定义 (my_service.thrift
) 中,字符串类型仍然使用 string
。
⚝ 在生成的 C++ 服务端代码中,将 MyMethod
函数的参数和返回值类型修改为 folly::fbstring&
和 const folly::fbstring&
。
⚝ 在服务实现中,直接使用 folly::fbstring
进行字符串操作。
注意:修改 Thrift 生成的代码可能需要在代码生成流程中进行调整,或者使用代码后处理脚本进行自动化修改。
▮ 使用 Folly 异步框架增强 Thrift 异步服务 (Using Folly Asynchronous Framework to Enhance Thrift Async Services):
Thrift C++ 库支持使用 TNonblockingServer
构建非阻塞服务器。可以结合 Folly 的 EventBase
和 Executor
来构建更高效的异步 Thrift 服务。
集成思路:
使用
folly::EventBase
替代 Thrift 的事件循环:Thrift C++ 库有自己的事件循环机制。可以尝试将 Thrift 的事件处理与 Folly 的EventBase
集成,或者直接使用EventBase
替代 Thrift 的事件循环。这可能需要深入理解 Thrift C++ 库的内部实现。使用
folly::Executor
处理 Thrift 请求:在 Thrift 服务端,可以使用 Folly 的Executor
(如ThreadPoolExecutor
) 来处理客户端请求,充分利用多核 CPU 资源。使用 Folly
Future
/Promise
进行异步操作:在 Thrift 服务端和客户端,可以使用 Folly 的Future
/Promise
模型来处理异步操作,例如异步调用其他服务、异步访问数据库等。
示例代码 (伪代码,仅为演示思路):
1
// ... Thrift 服务端代码 ...
2
#include <folly/executors/ThreadPoolExecutor.h>
3
#include <folly/futures/Future.h>
4
5
class MyAsyncServiceHandler : virtual public MyServiceCobSvIf { // 使用 CobSvIf 表示异步接口
6
public:
7
MyAsyncServiceHandler(folly::Executor* executor) : executor_(executor) {}
8
9
folly::Future<folly::fbstring> future_MyMethod(const folly::fbstring& request) override {
10
return folly::via(executor_, [request]() {
11
// 在 Folly Executor 上执行异步业务逻辑
12
folly::fbstring result = folly::toUpper(request);
13
return result;
14
});
15
}
16
17
private:
18
folly::Executor* executor_;
19
};
20
21
int main() {
22
// ... Thrift 异步服务端代码 ...
23
// 使用 TNonblockingServer
24
::std::shared_ptr<MyAsyncServiceHandler> handler(new MyAsyncServiceHandler(folly::getDefaultExecutor())); // 使用 Folly 默认 Executor
25
::std::shared_ptr<TAsyncProcessor> processor(new MyServiceAsyncProcessor(handler));
26
::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9090));
27
// ...
28
TNonblockingServer server(processor, serverTransport);
29
server.serve();
30
return 0;
31
}
代码解释:
⚝ 示例代码展示了如何在 Thrift 异步服务中使用 Folly 的 Executor
和 Future
。
⚝ MyAsyncServiceHandler
继承自 MyServiceCobSvIf
,表示异步服务接口。
⚝ future_MyMethod
函数返回 folly::Future<folly::fbstring>
,封装了异步操作的结果。
⚝ 使用 folly::via
将业务逻辑调度到 Folly 的 Executor
上执行。
④ 注意事项与最佳实践 (Precautions and Best Practices)
▮ Thrift 版本兼容性 (Thrift Version Compatibility):
确保 Folly 和 Thrift C++ 库的版本兼容。查看官方文档,了解版本兼容性信息。
▮ 代码侵入性 (Code Intrusiveness):
修改 Thrift 生成的代码可能需要较多的工作量,并且可能增加代码维护的复杂性。需要权衡性能提升和代码维护成本。
▮ 异步模型选择 (Asynchronous Model Choice):
根据项目需求选择合适的异步模型。Folly Futures 提供了更强大的异步控制能力,但可能需要更多的代码改动。
▮ 性能测试 (Performance Testing):
集成 Folly 后,进行充分的性能测试,验证性能提升效果,并找出潜在的性能瓶颈。
▮ 社区支持 (Community Support):
Folly 与 Thrift 的集成可能不像 Folly 与 gRPC 的集成那样成熟和广泛。在集成过程中遇到问题时,可能需要更多的自行研究和解决。
总而言之,将 Folly 与 Thrift 集成可以为 C++ Thrift 服务带来性能提升和开发效率的改善。然而,集成过程可能需要较多的技术细节处理和代码改造。在实际项目中,需要根据具体需求和团队技术能力来评估集成的可行性和收益。
Appendix A: 附录 A:Folly 常用类和函数速查表 (Appendix A: Quick Reference for Commonly Used Folly Classes and Functions)
本附录旨在为读者提供 Folly 库中常用类和函数的快速查阅指南。它并非详尽无遗的 API 文档,而是精选了在日常开发中使用频率较高、功能典型的类和函数,并以简洁明了的方式进行介绍,方便读者快速回忆和查找相关用法。对于更详细的用法和说明,请参考本书正文或 Folly 官方文档。
使用说明 (Instructions):
① 本速查表按照 Folly 库的主要模块进行分类,方便读者根据功能模块快速定位目标类或函数。
② 每个条目包含名称 (Name)、所属模块 (Module) 和 简要描述 (Description) 三个部分。
③ 名称 (Name) 列给出了类或函数的完整命名空间,例如 folly::fbstring
。
④ 所属模块 (Module) 列指示了该类或函数所属的 Folly 模块,例如 Strings (字符串)
。 这与本书的章节结构相对应,方便读者查阅正文中的详细介绍。
⑤ 简要描述 (Description) 列提供了对类或函数功能的概括性描述,力求简洁明了,突出其核心用途。
⑥ 对于部分常用的类或函数,速查表还提供了示例 (Example) 代码片段,帮助读者快速理解其基本用法。 示例代码仅为演示目的,可能并非完整可运行的代码。
⑦ 本速查表会持续更新和完善,以覆盖更多常用的 Folly 库组件。 欢迎读者提出宝贵的建议和反馈。
Appendix A1: 字符串处理 (String Processing)
① 类 (Classes)
▮▮▮▮ⓐ folly::fbstring
▮▮▮▮▮▮▮▮所属模块 (Module): Strings (字符串)
▮▮▮▮▮▮▮▮简要描述 (Description): 高效的字符串类,旨在替代 std::string
,提供优化的内存管理和性能。 特别是在处理小字符串和频繁拷贝的场景下表现更佳。 具有小字符串优化 (Small String Optimization, SSO)、写时复制 (Copy-on-Write, COW) 等特性。
▮▮▮▮ⓑ folly::StringPiece
▮▮▮▮▮▮▮▮所属模块 (Module): Strings (字符串)
▮▮▮▮▮▮▮▮简要描述 (Description): 非拥有的字符串视图,本质上是指向字符串的指针和长度的组合。 用于高效地操作字符串字面量或已存在的字符串,避免不必要的字符串拷贝。 常用于字符串解析、查找等场景。
▮▮▮▮ⓒ folly::format
▮▮▮▮▮▮▮▮所属模块 (Module): Strings (字符串)
▮▮▮▮▮▮▮▮简要描述 (Description): 类型安全的字符串格式化工具,类似于 std::printf
,但提供了类型检查和扩展性。 使用占位符进行格式化,支持自定义类型的格式化。
② 函数 (Functions)
▮▮▮▮ⓐ folly::to<T>(const StringPiece& str)
▮▮▮▮▮▮▮▮所属模块 (Module): Strings (字符串)
▮▮▮▮▮▮▮▮简要描述 (Description): 将 folly::StringPiece
或其他字符串类型转换为指定类型 T
的函数模板。 例如,可以将字符串转换为整数、浮点数等。 提供便捷的字符串到数值类型的转换。
▮▮▮▮▮▮▮▮示例 (Example):
1
folly::StringPiece str = "123";
2
int num = folly::to<int>(str); // num 的值为 123
▮▮▮▮ⓑ folly::stringPrintf(const char* format, ...)
▮▮▮▮▮▮▮▮所属模块 (Module): Strings (字符串)
▮▮▮▮▮▮▮▮简要描述 (Description): 类似于 std::sprintf
的字符串格式化函数,但返回 std::string
对象。 方便地将格式化后的字符串存储到 std::string
中。
▮▮▮▮▮▮▮▮示例 (Example):
1
std::string formatted_str = folly::stringPrintf("Hello, %s!", "World"); // formatted_str 的值为 "Hello, World!"
Appendix A2: 容器与数据结构 (Containers and Data Structures)
① 类 (Classes)
▮▮▮▮ⓐ folly::FBVector<T>
▮▮▮▮▮▮▮▮所属模块 (Module): Containers (容器)
▮▮▮▮▮▮▮▮简要描述 (Description): 优化的动态数组,旨在替代 std::vector
,提供更好的性能和内存管理。 尤其在频繁插入和删除元素的场景下可能更高效。
▮▮▮▮ⓑ folly::F14Map<Key, Value>
▮▮▮▮▮▮▮▮所属模块 (Module): Containers (容器)
▮▮▮▮▮▮▮▮简要描述 (Description): 高性能的哈希 map 容器,使用 F14 哈希算法,提供快速的查找、插入和删除操作。 通常比 std::unordered_map
具有更好的性能,尤其是在高负载情况下。
▮▮▮▮ⓒ folly::F14Set<T>
▮▮▮▮▮▮▮▮所属模块 (Module): Containers (容器)
▮▮▮▮▮▮▮▮简要描述 (Description): 高性能的哈希 set 容器,基于 F14Map
实现,同样使用 F14 哈希算法,提供快速的元素查找和插入操作。
▮▮▮▮ⓓ folly::PackedVector<T>
▮▮▮▮▮▮▮▮所属模块 (Module): Containers (容器)
▮▮▮▮▮▮▮▮简要描述 (Description): 节省空间的向量容器,通过位压缩技术存储数据,尤其适用于存储大量小整数的场景。 可以显著减少内存占用,但访问速度可能会略有下降。
Appendix A3: 异步编程 (Asynchronous Programming)
① 类 (Classes)
▮▮▮▮ⓐ folly::Promise<T>
▮▮▮▮▮▮▮▮所属模块 (Module): Asynchronous Programming (异步编程)
▮▮▮▮▮▮▮▮简要描述 (Description): 用于设置异步操作结果的对象,是 Future-Promise 模式中的 Promise 部分。 允许在异步操作完成时设置 Future 的值、异常或取消状态。
▮▮▮▮ⓑ folly::Future<T>
▮▮▮▮▮▮▮▮所属模块 (Module): Asynchronous Programming (异步编程)
▮▮▮▮▮▮▮▮简要描述 (Description): 表示异步操作结果的对象,是 Future-Promise 模式中的 Future 部分。 用于获取异步操作的结果,并支持链式操作、组合等功能。
▮▮▮▮ⓒ folly::Executor
▮▮▮▮▮▮▮▮所属模块 (Module): Asynchronous Programming (异步编程)
▮▮▮▮▮▮▮▮简要描述 (Description): 异步任务执行器接口,定义了任务调度的基本操作。 Folly 提供了多种 Executor
实现,如线程池、内联执行器等。
▮▮▮▮ⓓ folly::ThreadPoolExecutor
▮▮▮▮▮▮▮▮所属模块 (Module): Asynchronous Programming (异步编程)
▮▮▮▮▮▮▮▮简要描述 (Description): 基于线程池的 Executor
实现,用于并发执行异步任务,提高系统吞吐量。 可以配置线程池的大小、队列策略等。
② 函数 (Functions)
▮▮▮▮ⓐ future.then([](Try<T>&& result) { ... })
▮▮▮▮▮▮▮▮所属模块 (Module): Asynchronous Programming (异步编程)
▮▮▮▮▮▮▮▮简要描述 (Description): folly::Future
的成员函数,用于链式操作,在 Future 完成后执行回调函数。 回调函数接收 Try<T>
对象,表示 Future 的结果(可能是值或异常)。
▮▮▮▮▮▮▮▮示例 (Example):
1
folly::Future<int> future = ...;
2
future.then([](folly::Try<int>&& result) {
3
if (result.hasValue()) {
4
int value = result.value();
5
// 处理结果值
6
} else {
7
std::exception_ptr ex = result.exception();
8
// 处理异常
9
}
10
});
▮▮▮▮ⓑ folly::via(Executor* executor, std::function<R()> func)
▮▮▮▮▮▮▮▮所属模块 (Module): Asynchronous Programming (异步编程)
▮▮▮▮▮▮▮▮简要描述 (Description): 在指定的 Executor
上执行函数 func
,并返回一个 Future<R>
,表示函数执行的异步结果。 用于将同步函数转换为异步操作,并在指定的线程池中执行。
▮▮▮▮▮▮▮▮示例 (Example):
1
folly::Executor* executor = ...;
2
folly::Future<int> async_result = folly::via(executor, []() {
3
// 耗时操作
4
return 42;
5
});
Appendix A4: 网络编程 (Network Programming)
① 类 (Classes)
▮▮▮▮ⓐ folly::EventBase
▮▮▮▮▮▮▮▮所属模块 (Module): Network Programming (网络编程)
▮▮▮▮▮▮▮▮简要描述 (Description): 事件循环 (Event Loop) 的核心类,用于管理事件和执行事件处理程序。 是 Folly 异步 I/O 的基础,用于 I/O 多路复用、定时器管理等。
▮▮▮▮ⓑ folly::AsyncSocket
▮▮▮▮▮▮▮▮所属模块 (Module): Network Programming (网络编程)
▮▮▮▮▮▮▮▮简要描述 (Description): 异步套接字类,基于 EventBase
实现,提供非阻塞的网络 I/O 操作。 支持 TCP 和 UDP 协议,用于构建高性能的网络应用。
Appendix A5: 并发与同步 (Concurrency and Synchronization)
① 类 (Classes)
▮▮▮▮ⓐ folly::Mutex
▮▮▮▮▮▮▮▮所属模块 (Module): Concurrency and Synchronization (并发与同步)
▮▮▮▮▮▮▮▮简要描述 (Description): 互斥锁 (Mutex) 类,用于保护共享资源,实现线程同步。 提供排他性的锁,同一时刻只允许一个线程访问被保护的资源。
▮▮▮▮ⓑ folly::SharedMutex
▮▮▮▮▮▮▮▮所属模块 (Module): Concurrency and Synchronization (并发与同步)
▮▮▮▮▮▮▮▮简要描述 (Description): 共享互斥锁 (Shared Mutex) 类,也称为读写锁。 允许多个线程同时读取共享资源,但只允许一个线程写入共享资源,或在没有读者时写入。 适用于读多写少的场景。
▮▮▮▮ⓒ folly::ConditionVariable
▮▮▮▮▮▮▮▮所属模块 (Module): Concurrency and Synchronization (并发与同步)
▮▮▮▮▮▮▮▮简要描述 (Description): 条件变量类,用于线程间的等待和通知。 通常与互斥锁一起使用,允许线程在满足特定条件时被唤醒。
▮▮▮▮ⓓ folly::AtomicHashMap<Key, Value>
▮▮▮▮▮▮▮▮所属模块 (Module): Concurrency and Synchronization (并发与同步)
▮▮▮▮▮▮▮▮简要描述 (Description): 原子哈希 map 容器,提供线程安全的哈希 map 实现,允许在并发环境中进行原子操作。 使用无锁技术实现,通常比使用锁的哈希 map 具有更好的并发性能。
注意 (Notes):
① 本速查表仅列出部分常用的 Folly 类和函数,Folly 库的功能远不止于此。
② Folly 库版本更新迭代较快,部分 API 可能会有所调整。 请以实际使用的 Folly 版本为准。
③ 建议读者结合本书正文、Folly 官方文档和示例代码,深入学习和掌握 Folly 库的用法。
Appendix B: 附录B的标题
Appendix B:Folly 术语表 (Appendix B: Glossary of Folly Terms)
本附录旨在为读者提供本书中出现的 Folly 库相关术语的简明解释,帮助读者快速理解和查阅。
AsyncSocket (异步套接字)
▮▮▮▮Folly 库中用于异步网络编程的核心组件之一,AsyncSocket
提供了非阻塞的套接字接口,允许应用程序在不阻塞线程的情况下进行网络数据的发送和接收。它通常与 EventBase
结合使用,实现高效的事件驱动网络编程。
AtomicHashMap (原子哈希容器)
▮▮▮▮Folly 库提供的一种线程安全的哈希容器,AtomicHashMap
使用原子操作来实现并发访问的安全性,允许多个线程同时读取和修改哈希表,而无需显式的互斥锁,从而提高并发性能。
co_await (协程等待)
▮▮▮▮C++ 协程 (Coroutines) 中的关键字,用于暂停当前协程的执行,等待一个异步操作完成并返回结果。在 Folly 协程中,co_await
通常用于等待 Future
对象的结果,使得异步代码可以像同步代码一样顺序编写。
co_return (协程返回)
▮▮▮▮C++ 协程 (Coroutines) 中的关键字,用于从协程中返回值并结束协程的执行。在 Folly 协程中,co_return
可以返回一个值,或者在 folly::coro::Task
协程中,可以返回一个 Future
对象。
co_yield (协程产出)
▮▮▮▮C++ 协程 (Coroutines) 中的关键字,用于在协程中生成一个序列的值,类似于生成器 (generator)。在 Folly 协程中,co_yield
可以用于创建异步生成器,按需产生异步数据流。
ConditionVariable (条件变量)
▮▮▮▮Folly 库提供的条件变量实现,用于线程同步。条件变量允许线程在特定条件满足时挂起等待,并在其他线程发出通知时被唤醒。它通常与互斥锁 (Mutex) 结合使用,实现复杂的线程同步逻辑。
Coroutines (协程)
▮▮▮▮一种并发编程模型,允许程序在执行过程中挂起和恢复,从而实现非阻塞的异步操作。Folly 库提供了对 C++ 协程的支持,通过 co_await
、co_return
、co_yield
等关键字,可以简化异步代码的编写和维护,提高代码的可读性和可维护性。
emplace_back
▮▮▮▮C++ 容器(如 std::vector
和 folly::FBVector
)的成员函数,用于在容器的末尾直接构造元素,避免了先构造临时对象再拷贝或移动到容器中的开销,提高了效率,尤其是在元素类型构造开销较大时。
EventBase (事件基类)
▮▮▮▮Folly 库中事件循环的核心组件,EventBase
负责管理和调度事件,例如文件描述符事件、定时器事件等。它是构建高性能网络应用的基础,通过 I/O 多路复用 (I/O multiplexing) 技术,在一个线程中处理多个并发连接。
Executors (执行器)
▮▮▮▮Folly 库提供的任务调度和执行框架,Executor
定义了任务提交和执行的接口,Folly 提供了多种 Executor
的实现,如 ThreadPoolExecutor
(线程池执行器)、InlineExecutor
(内联执行器) 等,用于灵活地控制异步任务的执行方式和资源管理。
F14Map (F14 哈希Map)
▮▮▮▮Folly 库提供的高性能哈希 Map 容器,F14Map
使用优化的哈希算法和冲突解决策略,提供了比 std::unordered_map
更快的查找和插入性能,尤其是在高负载和大数据量场景下。
F14Set (F14 哈希Set)
▮▮▮▮Folly 库提供的高性能哈希 Set 容器,F14Set
与 F14Map
类似,使用相同的 F14 哈希算法和优化策略,提供了快速的元素查找和插入性能,适用于需要高效去重和成员检查的场景。
FBString (FB 字符串)
▮▮▮▮Folly 库提供的字符串实现,fbstring
旨在提供比 std::string
更高的性能和更低的内存开销。fbstring
采用了小字符串优化 (SSO)、引用计数和写时复制 (COW) 等技术,提高了字符串操作的效率,尤其是在字符串复制和修改频繁的场景下。
FBVector (FB 向量)
▮▮▮▮Folly 库提供的动态数组实现,FBVector
是 std::vector
的优化版本。它在内存分配策略、增长策略等方面进行了优化,旨在提供更高的性能和更低的内存开销。FBVector
适用于需要频繁插入和删除元素的场景。
folly (愚人)
▮▮▮▮Folly 库的顶层命名空间 (top-level namespace),所有 Folly 库的组件都位于 folly
命名空间或其子命名空间下。folly
本身在英文中意为“愚人”,这个名字来源于 Facebook 的早期文化,寓意着勇于尝试看似愚蠢但可能带来创新的想法。
folly::format (Folly 格式化)
▮▮▮▮Folly 库提供的类型安全的格式化工具,folly::format
类似于 std::printf
,但提供了类型安全和编译时检查,避免了格式化字符串错误导致的运行时问题。它还支持自定义类型的格式化,具有良好的扩展性。
Future (期物)
▮▮▮▮在异步编程中,Future
代表一个尚未完成的异步操作的结果。Folly 库提供了 Future
类,用于表示异步操作的结果,并提供了丰富的操作符 (如 then
, map
, flatMap
等) 用于链式处理异步结果,简化异步编程的复杂性。
InlineExecutor (内联执行器)
▮▮▮▮Folly 库提供的一种 Executor
实现,InlineExecutor
在提交任务的线程中直接同步执行任务,不进行线程切换。它适用于执行时间非常短的任务,可以避免线程切换的开销。
Lock-Free Programming (无锁编程)
▮▮▮▮一种并发编程技术,旨在避免使用互斥锁 (Mutex) 等锁机制来实现线程同步,而是使用原子操作 (Atomic Operations) 等更轻量级的同步原语。无锁编程可以减少线程阻塞和上下文切换的开销,提高并发程序的性能,但实现复杂且容易出错。
MemoryPool (内存池)
▮▮▮▮一种内存管理技术,预先分配一块大的内存区域,作为内存池。当需要分配内存时,从内存池中分配,释放内存时,将内存归还到内存池,而不是直接释放给操作系统。folly::MemoryPool
是 Folly 库提供的内存池实现,可以提高小对象频繁分配和释放的性能,并减少内存碎片。
Mutex (互斥锁)
▮▮▮▮Folly 库提供的互斥锁实现,用于保护共享资源,防止多个线程同时访问和修改共享资源导致的数据竞争 (data race) 问题。folly::Mutex
提供了独占锁的功能,同一时刻只允许一个线程持有锁并访问共享资源。
PackedVector (压缩向量)
▮▮▮▮Folly 库提供的一种节省空间的向量容器,PackedVector
通过位压缩 (bit packing) 技术,将多个小整数值压缩存储在较少的内存空间中。它适用于存储大量小整数的场景,可以显著减少内存占用。
Promise (承诺)
▮▮▮▮在异步编程中,Promise
用于设置一个 Future
对象的结果。Folly 库提供了 Promise
类,用于创建 Future
对象,并允许在异步操作完成后设置 Future
的值、异常或取消状态。Promise
和 Future
通常配对使用,用于异步操作的结果传递和同步。
SharedMutex (共享互斥锁)
▮▮▮▮Folly 库提供的共享互斥锁实现,也称为读写锁 (read-write lock)。folly::SharedMutex
允许多个线程同时持有读锁,但只允许一个线程持有写锁。它适用于读多写少的场景,可以提高并发读取的性能。
SSO (Small String Optimization, 小字符串优化)
▮▮▮▮一种字符串优化的技术,对于长度较短的字符串,直接将字符串数据存储在字符串对象自身的内存空间中,而不是在堆上分配内存。fbstring
和 std::string
通常都采用了 SSO 技术,以减少小字符串的内存分配和释放开销。
std::string (标准字符串)
▮▮▮▮C++ 标准库提供的字符串类,std::string
是 C++ 中最常用的字符串类型。虽然 std::string
功能强大且通用,但在某些高性能场景下,可能不如 folly::fbstring
效率高。
std::unordered_map (标准无序Map)
▮▮▮▮C++ 标准库提供的哈希 Map 容器,std::unordered_map
基于哈希表实现,提供了平均常数时间复杂度的查找、插入和删除操作。但在某些高性能场景下,folly::F14Map
可能提供更好的性能。
StringPiece (字符串片段)
▮▮▮▮Folly 库提供的字符串视图类,StringPiece
表示一个字符串的片段,但不拥有字符串的内存。StringPiece
只是指向字符串的指针和长度的组合,可以避免字符串的拷贝,提高字符串操作的效率,尤其是在字符串解析和查找等场景下。
ThreadPoolExecutor (线程池执行器)
▮▮▮▮Folly 库提供的一种 Executor
实现,ThreadPoolExecutor
使用线程池来执行提交的任务。线程池可以重用线程,减少线程创建和销毁的开销,提高异步任务的执行效率,尤其是在需要执行大量并发任务的场景下。
Thread-Local Storage (线程局部存储)
▮▮▮▮一种线程隔离的存储机制,允许每个线程拥有独立的变量副本。Folly 库提供了线程局部存储的支持,可以在多线程环境中安全地访问线程私有数据,避免数据竞争。
Appendix C: 参考文献与扩展阅读 (Appendix C: References and Further Reading)
附录 C:参考文献与扩展阅读 (Appendix C: References and Further Reading)
列出本书参考文献和扩展阅读资源,引导读者深入学习 Folly 库及相关技术。
本附录旨在为希望深入了解 Folly 库及其相关技术的读者提供一份详尽的参考文献和扩展阅读列表。通过这些资源,读者可以进一步探索 Folly 库的各个方面,并扩展其在 C++ 开发中的应用知识。
C.1 官方文档与资源 (Official Documentation and Resources)
本节列出了 Folly 库的官方文档和资源,这些是学习 Folly 最权威和最直接的途径。
① Folly 库 GitHub 仓库 (Folly Library GitHub Repository)
⚝▮▮▮- 链接: https://github.com/facebook/folly 🔗
⚝▮▮▮- 描述: Folly 库的官方 GitHub 仓库,包含了最新的源代码、文档、示例和 issue 跟踪。是获取 Folly 最新信息和参与社区讨论的首选之地。
② Folly 库官方文档 (Folly Library Official Documentation)
⚝▮▮▮- 链接: https://facebook.github.io/folly/docs/ 📄
⚝▮▮▮- 描述: 官方提供的在线文档,详细介绍了 Folly 库的各个模块、类和函数。是深入了解 Folly API 和用法的必备资源。
③ Facebook 开源 (Facebook Open Source)
⚝▮▮▮- 链接: https://opensource.facebook.com/projects/folly/ 🌐
⚝▮▮▮- 描述: Facebook 开源项目页面,提供了 Folly 库的简介、新闻和相关链接。可以从中了解到 Folly 在 Facebook 开源生态系统中的地位。
C.2 推荐书籍 (Recommended Books)
以下书籍可以帮助读者更深入地理解 C++ 编程、异步编程和并发编程,为更好地应用 Folly 库打下坚实的基础。
① 《Effective Modern C++》 (Effective Modern C++) by Scott Meyers ✍️
⚝▮▮▮- 描述: C++ 领域经典著作,深入讲解了 C++11 和 C++14 的新特性和最佳实践,包括移动语义 (move semantics)、lambda 表达式 (lambda expressions)、智能指针 (smart pointers) 等,这些知识对于理解和使用 Folly 库至关重要。
② 《C++ Concurrency in Action》 (C++ Concurrency in Action) by Anthony Williams 📚
⚝▮▮▮- 描述: 全面而深入地介绍了 C++ 并发编程,涵盖了线程 (threads)、互斥锁 (mutexes)、条件变量 (condition variables)、原子操作 (atomic operations)、future 和 promise 等关键概念和工具。对于理解 Folly 库的异步编程和并发模块非常有帮助。
③ 《深入理解现代计算机系统》 (Computer Systems: A Programmer's Perspective) by Randal E. Bryant and David R. O'Hallaron 💻
⚝▮▮▮- 描述: 从程序员的角度深入探讨计算机系统的各个方面,包括硬件、操作系统、编译器和网络。理解计算机系统底层原理有助于更好地理解 Folly 库的设计思想和性能优化策略。
④ 《高性能 MySQL》 (High Performance MySQL) by Baron Schwartz, Peter Zaitsev, and Vadim Tkachenko 🗄️
⚝▮▮▮- 描述: 虽然专注于 MySQL 数据库,但书中关于性能优化、并发控制和系统架构的原则和方法具有普适性,可以借鉴到 Folly 库的应用开发中,尤其是在构建高性能服务器程序时。
C.3 扩展阅读文章与博客 (Further Reading Articles and Blogs)
以下文章和博客提供了关于 Folly 库的深入分析、实践案例和应用技巧,可以帮助读者从不同角度了解和使用 Folly。
① Facebook Engineering Blog (Facebook Engineering Blog) 📰
⚝▮▮▮- 链接: https://engineering.fb.com/ 📰
⚝▮▮▮- 描述: Facebook 工程师团队的官方博客,经常发布关于 Folly 库和其他 Facebook 开源技术的文章,包括设计理念、技术细节、性能优化和应用案例。可以通过搜索 "Folly" 关键词找到相关文章。
② "Folly: Facebook Open-source Library" (Folly: Facebook Open-source Library) by InfoQ 📰
⚝▮▮▮- 链接: 在 InfoQ 网站上搜索 "Folly: Facebook Open-source Library" 🔍
⚝▮▮▮- 描述: InfoQ 上的相关文章,通常会对 Folly 库进行概括性介绍,并分析其在工业界的应用价值和技术特点。
③ CppCon 演讲视频 (CppCon Talk Videos) 🎬
⚝▮▮▮- 链接: https://www.youtube.com/results?search_query=CppCon+Folly 📺
⚝▮▮▮- 描述: CppCon 是 C++ 社区最重要的会议之一,经常有关于 Folly 库的演讲,内容涵盖 Folly 的各个模块和高级应用。通过观看这些演讲视频,可以更直观地了解 Folly 的技术细节和最佳实践。
④ Boost 库官方文档 (Boost Library Official Documentation) 📄
⚝▮▮▮- 链接: https://www.boost.org/doc/libs/1_85_0/libs/libraries.htm (以 Boost 1.85.0 版本为例,请查阅最新版本) 📄
⚝▮▮▮- 描述: Boost 库是 C++ 社区广泛使用的开源库集合,Folly 库在设计上借鉴了 Boost 的一些思想,并且与 Boost 有一定的互操作性。了解 Boost 库可以帮助更好地理解 Folly 的设计理念和使用方法。
⑤ 现代 C++ 教程和博客 (Modern C++ Tutorials and Blogs) ✍️
⚝▮▮▮- 描述: 网上有大量的现代 C++ 教程和博客,例如 https://zh.cppreference.com/w/ (cppreference.com) 和 https://isocpp.org/ (isocpp.org) 等,学习现代 C++ 语言特性是掌握 Folly 库的基础。
C.4 开源项目示例 (Open Source Project Examples)
通过研究使用 Folly 库的开源项目,可以学习 Folly 在实际项目中的应用方式和最佳实践。
① Proxygen (Proxygen)
⚝▮▮▮- 链接: https://github.com/facebook/proxygen 🚀
⚝▮▮▮- 描述: Facebook 开源的 C++ HTTP 框架,基于 Folly 库构建,是学习 Folly 网络编程模块 (AsyncSocket, EventBase) 的绝佳示例。
② Wangle (Wangle)
⚝▮▮▮- 链接: https://github.com/facebook/wangle 📐
⚝▮▮▮- 描述: Facebook 开源的 C++ 客户端/服务器框架,也基于 Folly 库构建,展示了如何使用 Folly 构建高性能的网络应用。
③ 其他使用 Folly 的开源项目 (Other Open Source Projects Using Folly) 🔍
⚝▮▮▮- 描述: 在 GitHub 上搜索 "folly" 关键词,可以找到许多使用 Folly 库的开源项目,研究这些项目可以了解 Folly 在不同领域的应用。
C.5 在线社区与论坛 (Online Communities and Forums)
参与在线社区和论坛是学习 Folly 库和解决问题的有效途径。
① Stack Overflow (Stack Overflow) 💬
⚝▮▮▮- 链接: https://stackoverflow.com/questions/tagged/folly 💬
⚝▮▮▮- 描述: Stack Overflow 上关于 Folly 库的标签页面,可以在这里提问、回答问题和搜索已有的解决方案。
② Folly 库 GitHub Discussions (Folly Library GitHub Discussions) 🗣️
⚝▮▮▮- 链接: https://github.com/facebook/folly/discussions 🗣️
⚝▮▮▮- 描述: Folly 库 GitHub 仓库的 Discussions 区域,可以参与关于 Folly 的讨论、提问和分享经验。
③ Reddit C++ 社区 (Reddit C++ Community) 🧑💻
⚝▮▮▮- 链接: https://www.reddit.com/r/cpp/ 🧑💻
⚝▮▮▮- 描述: Reddit 上的 C++ 社区,可以参与关于 C++ 和 Folly 库的讨论,获取社区的帮助和建议。
通过以上参考文献和扩展阅读资源,相信读者可以更全面、深入地学习和掌握 Folly 库,并在实际项目中灵活应用,提升 C++ 开发技能。 🚀