RocksDB ThreadLocalPtr梳理

关于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

StaticMeta是ThreadLocalPtr的核心类,它是以一个单例对象的形式存在的。核心功能是负责pthread_key_t的初始化与销毁工作,同时管理上层的线程存储对象,负责对象id的分配等。

ThreadData

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

Entry是作为每个ThreadLocalPtr实际获取数据的对象,其定义如下:

struct Entry {
  Entry() : ptr(nullptr) {}
  Entry(const Entry& e) : ptr(e.ptr.load(std::memory_order_relaxed)) {}
  std::atomic<void*> ptr;
};

ThreadLocalPtr

ThreadLocalPtr是最终的面向开发者的类,它的成员变量非常简单,只有一个:

const uint32_t id_;

该id在创建ThreadLocalPtr对象的时候由StaticMeta负责分配,在获取写入对应ThreadData的时候会通过此数据作为索引进行数据获取。对ThreadLocalPtr的所有操作,其实最终都是落到单例的StaticMeta中的。

我们看下假设我们需要通过一个ThreadLocalPtr写入一个数据,该数据的写入(Swap)流程:

  • 调用StaticMeta中的Swap,并且传入自身的id
  • 获取对应线程的ThreadData对象,假设不存在,则通过pthread_setspecific函数来新创建一个
  • 获取当前ThreadData的存储尺寸,假设当前id在尺寸方位之外,则会对存储进行扩容(扩展Entry)
  • 操作对应的Entry

所以假设当前有三个线程,每个线程都操作过同一个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

共 0 条回复
暂时没有人回复哦,赶紧抢沙发
发表新回复

作者

sryan
today is a good day