关于ThreadLocal,做过多线程编程的同学应当很熟悉了,通过ThreadLocal,我们可以在不同的线程中获取到对应的存储而不受其它线程影响。关于ThreadLocal,pthread主要提供了以下几个函数:
#include <pthread.h>
// Returns 0 on success, or a positive error number on error
int pthread_key_create (pthread_key_t *key, void (*destructor)(void *));
// Returns 0 on success, or a positive error number on error
int pthread_key_delete (pthread_key_t key);
// Returns 0 on success, or a positive error number on error
int pthread_setspecific (pthread_key_t key, const void *value);
// Returns pointer, or NULL if no thread-specific data is associated with key
void *pthread_getspecific (pthread_key_t key);
通过上述函数的注释,我们很容易发现一个pthread_key_t
对应了一个线程本地存储,多个线程通过同一个初始化过的pthread_key_t
可以获取到不同的变量。
清楚了大致的函数,我们就得知道为何RocksDB需要封装一个ThreadLocalPtr
类,是为了解决何种痛点呢?这里先说下我自己的结论,是为了方便通过一个pthread_key_t
来存储多个线程局部变量。在实际的RocksDB代码中,经常可以看到很多的ThreadLocalPtr对象,一个线程中不同的ThreadLocalPtr对象可以存储不同的对应的变量,但在底层他们对应的pthread_key_t
都是同一个。
StaticMeta是ThreadLocalPtr的核心类,它是以一个单例对象的形式存在的。核心功能是负责pthread_key_t
的初始化与销毁工作,同时管理上层的线程存储对象,负责对象id的分配等。
ThreadData是每个线程存储于pthread_key_t
中的数据结构,其定义如下:
struct ThreadData {
explicit ThreadData(ThreadLocalPtr::StaticMeta* _inst) : entries(), inst(_inst) {}
std::vector<Entry> entries;
ThreadData* next;
ThreadData* prev;
ThreadLocalPtr::StaticMeta* inst;
};
从上述的数据结构可以看到,最终ThreadData会组成一个双向链表,而该链表则由StaticMeta来进行管理,主要为了销毁pthread_key_t
的时候可以遍历该链表,对于每个内部对象进行解除引用操作。
Entry是作为每个ThreadLocalPtr实际获取数据的对象,其定义如下:
struct Entry {
Entry() : ptr(nullptr) {}
Entry(const Entry& e) : ptr(e.ptr.load(std::memory_order_relaxed)) {}
std::atomic<void*> ptr;
};
ThreadLocalPtr是最终的面向开发者的类,它的成员变量非常简单,只有一个:
const uint32_t id_;
该id在创建ThreadLocalPtr对象的时候由StaticMeta负责分配,在获取写入对应ThreadData的时候会通过此数据作为索引进行数据获取。对ThreadLocalPtr的所有操作,其实最终都是落到单例的StaticMeta中的。
我们看下假设我们需要通过一个ThreadLocalPtr写入一个数据,该数据的写入(Swap)流程:
pthread_setspecific
函数来新创建一个所以假设当前有三个线程,每个线程都操作过同一个ThreadLocalPtr对象,则最终会有三个Entry被生成,具体的看下ThreadData的官方注释更加清楚:
// This is the structure that is declared as "thread_local" storage.
// The vector keep list of atomic pointer for all instances for "current"
// thread. The vector is indexed by an Id that is unique in process and
// associated with one ThreadLocalPtr instance. The Id is assigned by a
// global StaticMeta singleton. So if we instantiated 3 ThreadLocalPtr
// instances, each thread will have a ThreadData with a vector of size 3:
// ---------------------------------------------------
// | | instance 1 | instance 2 | instance 3 |
// ---------------------------------------------------
// | thread 1 | void* | void* | void* | <- ThreadData
// ---------------------------------------------------
// | thread 2 | void* | void* | void* | <- ThreadData
// ---------------------------------------------------
// | thread 3 | void* | void* | void* | <- ThreadData
// ---------------------------------------------------
由此可以看出,假设我们需要存储多种不同用途的对象,只需要创建对应数量的ThreadLocalPtr对象即可,而不需要创建多个底层的pthread_key_t
。