微信号:Engineer_First

介绍:坚持分享一线攻城狮的实践、学习心得 路漫漫兮修远矣,一路同行

Folly源码分析系列(一) — ThreadLocalPtr

2016-04-30 19:49 icy

Folly( Facebook Open Source Library)是Facebook在2012年开源的C++11组件库。它以实用性和高效性为设计中心,提供了类似booststl的功能,包括bitset,散列,字符串等。

ThreadLocalPtr作为Folly库内置的一种智能指针,提供了与boost::thread_specific_ptr相似的功能:访问线程TLS(Thread Local Storage)数据的接口。通过ThreadLocalPtr变量可以在每个线程TLS中存储一份彼此互相隔离的自定义数据。用户可以通过get()接口获取当前线程的数据,也可以通过accessAllThreads()接口,遍历一个ThreadLocalPtr变量的所有线程的TLS数据。

1、ThreadLocalPtr API

ThreadLocalPtr是一个pointer,该pointer在不同的Thread中访问不同的数据。它提供了下面的api。

  • Default Constructor:构造一个空的ThreadLocalPtr对象,不指向任何对象;

  • Move constructor:移动构造函数;

  • Destructor:销毁所有Thread中this指向的对象,并回收索引;

  • Move assignment operator:首先销毁所有Thread中this指向的对象,并回收索引;然后使得this指向参数other的数据,other不再指向任何数据;

  • reset:销毁this在当前线程中的TLS数据,并指向新的数据;

  • get:返回this指向的TLS数据;

  • release:返回this指向的TLS数据,同时使得this不再指向该数据;

  • operator->:返回一个指向TLS数据的普通指针;

  • operator*:直接返回普通数据;

  • operator bool():通过ThreadLocalPtrbool的类型转换;

需要说明的是:通过禁用 copy constructor 和 copy assignment operator=(),使得ThreadLocalPtr成为non-copyable的,使得没有两个ThreadLocalPtr指向同一个TLS数据。

2、设计实现

程序维护一个TLS的指针数组,每个指针指向一个对象。ThreadLocalPtr保存一个指向指针数组的索引。它们的关系如下图所示:


(1) ThreadLocalPtr

表示一个TLS pointer,有一个private的数据成员id_保存索引。可以通过该索引访问ThreadEntry::elements数组的第id_个数据。

(2) StaticMeta<Tag>

StaticMeta<Tag>一个用class template实现的 sigleton 。对于每个类型,StaticMeta<Tag>类模板会创建出不同的类。例如:StaticMeta<String>,表示一个模板类型为StringStaticMeta singleton实例。 StaticMeta<Tag>主要提供三个方面的功能:

  • 初始化ThreadLocalPtrid_

这个id_值是通过StaticMeta<Tag>成员函数static uint32_t create()返回。StaticMeta<Tag>维护一个unit32_t类型的数据nextId_,表示下一个可用的索引;同时维护一个vector<unit32_t>freeIds_,保存已经被销毁的ThreadLocalPtr的索引。如果freeIds_不为空,那么create()将返回freeIds_中的最后一个索引;否则,返回nextId_,并把nextId_加一。

注意create()通过StaticMeta<Tag>::lock_来保证对nextId_freeIds_的访问是线程安全的。

  • 提供访问当前Thread的TLS数据的接口

StaticMeta<Tag>利用ThreadEntry来存储一个线程的TLS数据,并通过成员函数static ThreadEntry* getThreadEntry()返回该对象。StaticMeta<Tag>有一个数据成员:pthread_key_t pthreadKey_,表示一个TLS数据。为了提高性能,getThreadEntry成员函数在不同的平台采用了不同的实现:

  1. 使用特定编译器定义的TLS关键字: StaticMeta<Tag>通过TLS关键字声明了一个static的TLS数据成员: ThreadEntry threadEntry_。GNUC下通过__thread关键字声明,MSVC下通过__declspec(thread)关键字声明。getThreadEntry()直接返回threadEntry_

  2. 在其他的编译器平台可用pthread接口:getThreadEntry()通过pthread_getspecific(pthreadKey_)来获取TLS数据。

注意:在支持c++11的平台上,也可以尝试用thread_local特性来实现。 

  • 提供遍历访问所有Thread的TLS数据的接口

StaticMeta<Tag>::head_指向一个双向链表的表头,链表的元素是ThreadEntry类型。一个ThreadEntry对象存储一个线程的TLS数据(详见对ThreadEntry的解释)。StaticMeta<Tag>::lock_保证对head_的访问是线程安全的。

ThreadEntry对象的创建和加入链表有两种情况:

  1. 当一个进程的第一个子线程被创建时(如fork一个子进程之后,该子进程的主线程),在该子线程中onForkChild()函数里创建对应的ThreadEntry,并加入到head_指向的链表中。

  2. 当一个进程的其他子线程被创建时,不会立即创建对应的ThreadEntry。只有当访问到它的TLS数据时,才开始创建它的ThreadEntry对象,并在创建成功后加入到head_中( 具体逻辑参见StaticMeta<Tag>::get(id)StaticMeta<Tag>::reserve())。

ThreadEntry对象的销毁以及从链表中删除:

当一个线程退出时,StaticMeta<Tag>::onThreadExit()销毁ThreadEntry中保存的TLS数据和ThreadEntry本身,并从head_链表中删除它。

注意:onForkChild()中,因为当前子进程只有一个线程,所以对head_的访问不存在并发,不需要保护;但是在onThreadExit()时,当前线程未必是最后一个线程,因此需要lock_的保护。

(3) ThreadEntry类

一个ThreadEntry对象存储一个线程的TLS数据。每个TLS数据用ElementWrapper对象来表示。ThreadEntry负责维护一个ElementWrapper类型的动态数组,elements指向数组头部,elementsCapacity表示数组大小。所有的ThreadEntry组成一个双向链表,由StaticMeta<Tag>::head_指向链表头。

注意:此处可与Java语言中的ThreadLocal采用的存储策略互作参考。Java中的ThreadLocal变量,是存储到Java线程Thread类中的ThreadLocalMap属性中。而ThreadLocalMap是以ThreadLocal变量为Key,具体的TLS数据为Value进行存储。而Java是基于GC的语言,在ThreadLocalMap中还特别通过WeakReference来引用ThreadLocal Key,Hint垃圾回收器在特殊时候进行资源释放。

(4) ElementWrapper类

void*指针的包装,使得用户可以通过自己的deleter自定义对象生命周期结束时的销毁策略。

3、可供参考学习的Tips

(1) 用static变量来实现singleton

static StaticMeta<Tag>& instance() {
  static bool constructed = (inst_ = new StaticMeta<Tag>());
  (void)constructed; // suppress unused warning
  return *inst_;
}

代码片段中constructed是一个static变量,语言规范保证在instance()函数第一次执行的时候进行初始化,且仅执行一次。从而保证了inst_只会被初始化一次。

(2) accessThreads()接口的使用

for(const auto& i : val_.accessAllThreads()) {
  ret += i;
}  

作为一种C++11中出现的syntax sugar,上述代码片段等同于:

Accessor accessor = val_.accessAllThreads();  // 获取StaticMeta<Tag>::lock_指示的锁
for(const auto &i = accessor.begin(); i != accessor.end(); ++accessor) {  // accessor.begin()返回一个Iterator对象,因此i是Accessor::Iterator类型
  ret += i;
}

可比较的是,这与Java语言中的Iterable类似,只要标识的某个东西是可迭代的,从而就可用在for循环语句中进行迭代。这就不知道是谁借鉴谁。^_^


  1. 文章同步post在Github: ThreadLocalPtr | https://github.com/halty/writing/blob/master/Folly_Source_Insight_Series-ThreadLocalPtr.md

  2. 扫描下面二维码,关注公众号[一线工程师],不定时干货分享!^_^


 
一线工程师 更多文章 聊聊代码规范
猜您喜欢 Eclipse,到了说再见的时候了——Android Studio最全解析 国内第一本《Vue.js权威指南》来啦,来自滴滴出行-公共前端团队,作者签名版限量包邮 50 本 Linux 概念架构的理解 【运维精英群】云络科技王寒:DevOps的最佳实践与Docker的运维挑战 程序员的幽默笑话