Facebook Folly源代码分析

article/2025/11/5 20:45:39
Folly是Facebook的一个开源C++11组件库,它提供了类似Boost库和STL的功能,包括散列、字符串、向量、内存分配、位处理等,用于满足大规模高性能的需求。 6月初,Facebook宣布将其内部使用的底层C++组件库Folly开源,本文尝试对Folly库中的几个重要的数据结构代码进行分析,包括一些实现细节的讨论、特点和不足的分析,以及在工程上的应用。本文将首先分析RWSpinlock.h和ThreadLocal.h的源代码。 RWSpinlock.h 顾名思义,RWSpinlock就是使用自旋方式进行临界区资源等待的读写锁,它与pthread_rwlock_t有三个比较重要的区别。
  • 通常情况下等待锁的线程不会主动让出CPU,而是循环中不断地尝试获取锁。
  • 使用原子操作处理读者计数或写者状态,避免pthread_rwlock_t无论读写的加锁解锁都要在互斥锁的保护下进行。
  • 提供类似数据库中”更新锁”的机制,在尽量提供更高并发的情况下,避免死锁。 [caption id="attachment_12592" align="aligncenter" width="400" caption="表1 读写锁状态相容矩阵"][/caption]
RWSpinlock实现中几处值得关注的地方如下。 自旋锁在锁粒度较小的情况下,使用自选等待方式等锁,可以避免较高的上下文切换代价。而为了自适应多次获取锁失败的情况,可以主动让出CPU。Folly的 实现比较简单,硬编码为在自旋1000次后仍无法获取锁的情况下,以后的每次循环都调用sched_yield主动让出CPU以调度其他线程上来运行(要 研究更复杂的自适应锁机制,可以参考Solaris内部实现的Adaptive locks或注1提到的论文)。但在获取读锁次数远大于写锁次数的情况下,RWSpinlock的读优先机制可能造成写者的饥饿,而主动让出CPU的机制 则可能加重写者的饥饿程度。因此Folly中同时实现了可配置为写者优先的RWTicketSpinLockT锁。 与通常对读计数器加1的 思路不同,RWSpinlock使用int32_t的高30位保存读计数,而使用最低两位保存upgrade和write标记。在加解读锁时直接对 int32_t的锁状态原子加减0x4,直接避开了对最低两位的修改,执行原子加0x4后再根据原子操作前的最低两位是否有效,来决定是否需要回滚(减 0x4)。在多数只获取读锁的情况下,不需要回滚,一次ATOMIC_ADD即完成读锁的加解锁。 [caption id="attachment_12609" align="aligncenter" width="436" caption="图1 使用比ATOMIC_CAS更高效的ATOMIC_ADD处理读锁的加锁和解锁"]
[/caption] Folly中的RWSpinlock还提供了类似数据库中“更新锁”的upgrade锁,用于对锁保护对象先读后改时避免死锁的需求,它与读写锁的状态相容矩阵如表1所示。 Write/Read/Upgrade三种锁状态除了可以和初始状态进行加解锁的双向转换外,也可以在某些锁状态之间进行转换,即原子的释放原来的锁并获取到新的锁,锁状态转换如图2所示。 [caption id="attachment_12599" align="aligncenter" width="431" caption="图2 锁状态转换"]
[/caption] 可以看出,除读写状态外,其他任意两个状态之间都是双向的转换,只有读写之间是单向转换,即在持有写锁的情况下,可以降级为读锁,而在持有读锁的情 况下却不能升级为写锁。原因很简单,在两个及以上线程都持有读锁,并尝试获取写锁的情况下,由于释放读锁和获取写锁必须原子性的完成,而要获取写锁就要等 待其他线程释放读锁,在这种情况下线程将进入死锁状态。 因此某些对锁保护对象需要先读取再决定是否修改的情况,只能在读取之前就加上写锁, 而在读取后需要修改的情况很少时,这种方式代价就比较大,因为它阻塞了其他线程获取读锁。Upgrade就应运而生,从相容性矩阵可以看到,Upgrade锁与读锁相容,而与其他upgrade和写锁不相容,线程在读取数据之前可以先获取upgrade,读取数据之后,如果决定需要修改, 就升级为写锁。 一个具体的使用示例如下,如图3所示,考虑实现一个在结点上加锁的B+tree。 [caption id="attachment_12601" align="aligncenter" width="362" caption="图3 结点加锁B+tree查询"]
[/caption] 查询操作,按照如下操作顺序从根节点开始加锁和查询: 1. 对父节点加读锁; 2. 获取子节点的读锁; 3. 释放父节点读锁。 继续在当前节点上执行第二步,直到查询到叶子节点。 更新操作,按照如下操作顺序从根节点开始加锁和查询: 1. 对父节点加upgrade锁; 2. 获取子节点的upgrade锁; 3. 判断子节点如果可能需要分裂或合并,升级父子节点为写锁; 4. 执行子节点分裂或合并,并修改父节点内容; 5. 释放父节点锁; 6. 继续在当前节点上执行第二步,直到叶子节点,对叶子节点执行插入/删除/修改。 因此在B+tree的应用中,由于索引节点的分裂合并操作比较少见,使用upgrade锁,避免与读锁的竞争,只有在必要时才升级为写锁。 关 于C++0x中atomic对象中的memory_order,RWSpinlock使用std::atomic保存上面提到的读锁计数、写锁标记、 upgrade锁标记,使用了fetch_add、fetch_and、fetch_or、compare_exchange_strong这几个原子操 作函数来修改锁状态。作者在不同的场景下使用了三种不同的memory_order,与作者沟通,他的解释如下:
For example, unlock_shared() can be delayed to other memory_order_release (or memory_order_relaxed), but not memory_order_acquire, which means it ok for the compiler (and machine) to reordering unlock_shared() from different threads.
但从gcc4.6.3中std::atomic的实现和生成的汇编代码来看,上面提到的几个原子操 作函数,直接使用了gcc提供的几个以__sync开头的内置原子操作,忽略掉了传入的memory_order参数。而只有store函数的行为针对不 同的memory_order只有是否增加mfence指令的差别。最后,笔者的建议是在性能影响不大的的情况下,直接使用std::atomic默认的 高级别的memory_order,因为通过分析复杂的原子操作指令优化时序,来决定memory_order,收益可能不及它带来的风险。 写 者优先的RWTicketSpinLockT锁,提供写锁优先的调度机制,在有线程等待获取写锁的情况下,不再授予读锁,避免在大量加读锁的场景下,写锁 很难获取的问题。使用了gcc内置的原子操作__sync_fetch_and_add和__sync_bool_compare_and_swap替代 std::atomic,并且也没有用到其他C++0x特性,使用旧版本gcc的项目可以使用这个锁。 Folly注重效率,因此RWTicketSpinLockT中也有几处值得关注的细节。
  • 在等待获取锁的自旋中使用pause指令,一方面可以降低CPU的功耗,另一方面还可以帮助CPU优化指令流效率,具体可以参考注2的Intel白皮书。 而在写锁不优先的情况下,由于pause带来的延迟可能导致写锁更不容易被获取,因此获取非优先的写锁不使用pause指令。
  • 使用SSE并行指令,对多个地址连续的整数一个指令完成++操作。
  •  减少分支判断。见源码try_lock_shared()的old.users = old.read。将users与read是否相等的逻辑延迟到CAS操作时顺便判断,尽管在加不上读锁的情况下,要多执行两个自加和一个CAS操作,但 在加读锁成功的多数情况下,省去了一次分支判断。
  • 使用__sync_fetch_and_add代替 __sync_bool_compare_and_swap。RWTicketSpinLockT使用了名为Write、Read、User的三个计数器 用来保存读锁计数和写锁标记,方法比较巧妙。读锁需要在user等于read的情况下才可以加上,而写锁则需要满足user等于write,加解锁逻辑如下。
1. 通过对read和user原子加一,获取读锁,同时封锁了加写锁的条件。 2. 通过对read原子加一,获取写锁,同时也就封锁了加读锁的条件;这里通过先对read加一,封锁了后续读锁的条件,然后再等待写锁的条件被满足,实现了写锁优先的逻辑。 3. 通过对write原子加一,释放读锁,同时恢复写锁的加锁条件。 4. 通过对read和write原子加一,释放写锁,同时恢复读锁的加锁条件。 可以看出写锁的获取和读锁的释放可以避免使用CAS,而用一个原子加即可实现。 ThreadLocal.h 在服务器编程中,通常会遇到需要为每个线程都分配不同对象的情况,如线程处理一个请求需要使用的临时内存、远程调用需要临时构造的参数等等。在需要的时候临 时构造,不仅要付出构造成本,还会有内存申请释放的代价,而使用线程主函数的栈对象,每一层都要传递参数也让代码很不便维护。 Folly中实现的ThreadLocal.h提供了对象的线程局部存储和访问,其功能与pthread_getspecific相似,提供了更方便友好的调用方式, 线程退出后自动析构本线程内所欲的私有对象,并且提供遍历所有线程私有对象的接口。实现上使用了GCC内置特性,实现比pthread库更快的线程私有对 象访问。 ThreadLocal内存布局如图4所示,主要由StaticMeta、ThreadEntry和ElementWrapper三者组成。 [caption id="attachment_12603" align="aligncenter" width="413" caption="图4 ThreadLocal内存布局"]
[/caption]
  • StaticMeta为全局唯一结构,主要包括各个线程管理结构组成的链表的头指针和对象ID生成器,用于全局析构和遍历所有线程的私有对象。
  • ThreadEntry为线程私有结构,每线程对应一个,主要包括线程线程私有对象的指针数组,管理所有线程私有对象的指针,通过ID获取指定对象的指针。
  • ElementWrapper是线程私有对象管理器,每个对象实例对应一个,主要包括指向对象实例的指针和对象的析构方法。
假设要管理的线程私有类型为T,初始化和访问线程私有对象的流程如下。 1. ThreadLocalPtr对象构造时即从StaticMeta为它管理的类型申请了唯一ID。 2. 使用TheadLocalPtr::get方法通过ID从ThreadEntry管理的ElementWrapper数组中获取一个ElementWrapper对象。 3. 如果ElementWrapper中T的指针为空,则构造一个T的对象,指针和析构方法保存在ElementWrapper对象中。 4. 如果ElementWrapper中的T指针不为空,则直接返回。 在 Folly的实现中值得注意的是:ThreadEntry对象在每个线程中有一个,使用gcc内置的static __thread方式声明ThreadEntry,即可实现同一个名字在不同线程访问到的是不同对象,需要注意的是这种方式仅适用于POD类型。由于直接 访问对象,这种方式比调用pthread库的pthread_getspecific函数调用方式效率要更高。 但由于 ThreadEntry是POD类型,在线程退出时不能自动析构释放它管理的线程私有对象,因此在StaticMeta构造时会申请一个 pthread_key_t注册线程退出时的回调函数,在回调函数中遍历当前线程ThreadEntry管理的所有私有对象,依次调用它们的析构方法。因 此在Folly的实现中,对pthread库函数仅仅使用了它在线程退出时调用回调函数的功能。 此外还有两处细节值得借鉴。 ●ThreadLocal还区分了单个线程退出和整体析构情况下,传给析构方法不同的参数,以便用户在必要的情况下区别这两种情况下析构方法的实现。 ●提供了指定立即析构释放当前线程私有对象的方法,而不必等待线程退出时才释放,这一点在单元测试多个case的情况下可能会使测试变得比较方便。 注释 注1: Adaptive Locks: Combining Transactions and Locks for Efficient Concurrency 注2: Using Spin-Loops on Intel® Pentium® 4 Processor and Intel® XeonTM Processor 注3: A Provably Correct Scalable Concurrent Skip 注4: Hazard Pointers: Safe Memory Reclamation for Lock-Free Objects 注5: Doug Lea. ConcurrentSkipListMap. In java.util.concurrent   作者李凯,现任淘宝核心系统部存储组技术专家,花名郁白。2007-2010年曾参与百度分布式文件系统研发,2010年至今参与淘宝Oceanbase项目研发。
本文选自《程序员》杂志2012年07期,未经允许不得转载。如需转载请联系 market@csdn.net 《程序员》2012年杂志订阅送好礼活动火热进行中
 

http://chatgpt.dhexx.cn/article/IlTrsZZN.shtml

相关文章

folly官方例子

folly官方例子 Future<vector<LeafResponse>> fanout(const map<Leaf, LeafReq> &leafToReqMap,chrono::milliseconds timeout) {vector<Future<LeafResponse>> leafFutures;for (const auto &kv : leafToReqMap) {const auto &leaf…

Facebook 的 C++ 11 组件库 Folly Futures

英文原版&#xff1a;https://code.facebook.com/posts/1661982097368498/futures-for-c-11-at-facebook/ https://www.oschina.net/translate/futures-for-c-11-at-facebook http://www.lupaworld.com/article-254822-1.html Futures 是一种通过自然的、可组合的方式表达异…

交叉编译folly库

假定交叉编译链工具所在目录为&#xff1a;/home/softwares/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/&#xff0c;其c编译器为&#xff1a;/home/softwares/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/aarch64-linux-gnu-g 1. 下载folly源码&…

folly库安装(5)folly的安装

上面这些准备工作做完了&#xff0c;现在就可以安装folly了&#xff0c;其实这时folly的安装已经非常顺利了。网上有人说folly的安装很麻烦&#xff0c;最重要是上面的准备工作没做好&#xff0c;只要你按照我上面的文章&#xff0c;一步步做下来&#xff0c;安装成功是没问题的…

揭秘Facebook官方底层C++函数Folly

2019独角兽企业重金招聘Python工程师标准>>> Folly与Boost、当然还有std等组件库的关系是互为补充&#xff0c;而不是彼此竞争。实际上&#xff0c;只有当我们需要的东西既没有&#xff0c;也无法满足所需的性能要求时&#xff0c;我们才开始定义自己的组件。 性能问…

《设计原则》(一)

易理解性和易使用性的设计原则 提供一个好的概念模式&#xff1b;&#xff08;一个好的概念模式使用户能够预测操作的行为效果&#xff09;可视性(消除执行阶段和评估阶段的鸿沟)&#xff1b;自然匹配&#xff1b;&#xff08;利用物理环境类比和文化标准概念、空间类比&#…

C++设计模式的设计原则(面向对象八大设计原则)

面向对象设计八大设计原则 一、温故面向对象二、八大设计原则三、以史为鉴 先掌握八大设计原则&#xff0c;再详细看23种设计模式&#xff08;&#x1f448;点我&#xff09; 一、温故面向对象 &#xff08;1&#xff09;隔离变化&#xff1a;从宏观层面上来看&#xff0c;面向…

设计原则设计模式

导论 什么是设计原则&#xff1a;判断程序设计质量好坏的准则。什么是设计模式&#xff1a;软件设计过程中重复出现问题的解决方案设计原则的作用&#xff1a;指导抽象、类、类关系设计&#xff0c;相当于指导设计程序基础框架&#xff08;Rank-分层、Role-角色、Relation-类关…

设计原则详解

1.单一职责 一个类&#xff0c;只有一个引起它变化的原因。应该只有一个职责。每一个职责都是变化的一个轴线&#…

五大设计原则——SOLID

目录 简介&#xff1a; 1、单一职责原则&#xff08;SRP&#xff09; 2、开闭原则&#xff08;OCP&#xff09; 3、里式替换原则&#xff08;LSP&#xff09; 4、依赖倒置原则 (DIP) 5、接口隔离原则 (ISP) 简介&#xff1a; 无论是软件系统设计&#xff0c;还是代码实现…

1. 设计原则

文章目录 设计原则思维导图核心理论SOLID单一职责开放封闭里式替换接口隔离依赖反转 KISSDRYLOD 设计原则思维导图 核心理论 基于接口编程 “基于接口而非实现编程” - “Program to an interface, not an implementation”。 “接口”就是一组“协议”或者“约定”&#xff…

七大设计原则

一、七大设计原则 &#xff08;1&#xff09;单一职责原则 &#xff08;2&#xff09;接口隔离原则 &#xff08;3&#xff09;依赖倒置原则 &#xff08;4&#xff09;里氏替换原则 &#xff08;5&#xff09;开闭原则 &#xff08;6&#xff09;迪米特法则 &#xff0…

chrome浏览器截长图

使用chrome浏览器 打开开发者模式(更多工具->开发者工具) mac 按commandshiftp windows 按ctrlshiftp 然后输入capture 选择capture full size screenshot就可以了 截了个长图的例子

手把手教你截长图

1.截长图的工具 相信很多小伙伴在平时工作做都会碰见截图的问题&#xff0c;那正常的图&#xff0c;我们有各种方式去截取&#xff0c;例如&#xff1a;QQ的CtrlAltA&#xff0c;微信的AltA等等 但是呢&#xff0c;如果要用到长图的时候&#xff0c;就束手无策了&#xff0c;这…

python如何截长图_利用 Python + Selenium 实现对页面的指定元素截图(可截长图元素)...

对WebElement截图 WebDriver.Chrome自带的方法只能对当前窗口截屏&#xff0c;且不能指定特定元素。若是需要截取特定元素或是窗口超过了一屏&#xff0c;就只能另辟蹊径了。 WebDriver.PhantomJS自带的方法支持对整个网页截屏。 下面提供几种思路。 方式一 针对WebDriver.Chro…

谷歌浏览器怎么截长图?

我们在使用电脑浏览网页的时候难免会需要进行一些长图的截取&#xff0c;而一般的截图只能实现一部分截取&#xff0c;那么我们要如何去实现这个操作呢&#xff1f;下面小编就给大家介绍一下怎么在谷歌浏览器上截长图的操作。 谷歌浏览器网页截长图怎么截&#xff1f; 1、进入C…

html2canvas截长图

github链接 一、下载运行后选择下图的html2canvas即可直接去到路由界面测试 二、下图是html2canvas路由页面&#xff0c;点击右上角的生成图片即可下载长图 三、源码路径&#xff08;html2canvas源码github&#xff09; 四、源码&#xff08;关键在generateImage 这个方法&…

selenium+phantomjs截长图踩坑

目录 需求背景&#xff1a; 调研 phantomjs selenium 服务器部署 需求背景 BI上的报表需要设置定时任务截图发邮件到订阅人的邮箱中。刚开始以为截图的活是前端的&#xff0c;后来发现使自己的锅。 调研 截图的研究了一下&#xff0c;主流应该是 selenium 和 phantomjs。…

microsoft edge怎么截长图_实用技能 | Fireshot 网页截长图工具

FireShot 网页截长屏插件 网页截图有没有什么好方法? 在我们平常的工作、生活和学习中,截图是我们最常用到的功能之一。小编平常用到的是QQ、微信、电脑自带的快捷截图功能以及红蜻蜓截图软件等。 但是在浏览网页时,这些工具功能出现了一个致命的缺点,看到一个长长的文章,…

snipaste怎么滚动截长图_如何截长图,这3种方法你用过吗?

在工作中,经常需要截长图,那在电脑上你一般是如何操作呢?本期Word妹与大家分享2种快速截图技巧。 1、借用QQ工具 在最新的QQ版本中有一个长截图按钮,点击之后直接拉动需要长截图的内容,最后点击完成即可。 2、借用FastStone Capture工具 FSCapture是绿色版本不需要安装,可…