概述

在开发过程中,我们经常会遇到并发问题,解决并发问题通常的方法是加锁保护,比如常用的spinlock,mutex或者rwlock,当然也可以采用无锁编程,对实现要求就比较高了。对于任何一个共享变量,只要有读写并发,就需要加锁保护,而读写并发通常就会面临一个基本问题,写阻塞读,或则写优先级比较低,就会出现写饿死的现象。这些加锁的方法可以归类为悲观锁方法,今天介绍一种乐观锁机制来控制并发,每个线程通过线程局部变量缓存共享变量的副本,读不加锁,读的时候如果感知到共享变量发生变化,再利用共享变量的最新值填充本地缓存;对于写操作,则需要加锁,通知所有线程局部变量发生变化。所以,简单来说,就是读不加锁,读写不冲突,只有写写冲突。这个实现逻辑来源于Rocksdb的线程局部缓存实现,下面详细介绍Rocksdb的线程局部缓存ThreadLocalPtr的原理。

线程局部存储(TLS)

简单介绍下线程局部变量,线程局部变量就是每个线程有自己独立的副本,各个线程对其修改相互不影响,虽然变量名相同,但存储空间并没有关系。一般在linux 下,我们可以通过以下三个函数来实现线程局部存储创建,存取功能。

int pthread_key_create(pthread_key_t *key, void (*destr_function) (void*)),
int pthread_setspecific(pthread_key_t key, const void *pointer) ,
void * pthread_getspecific(pthread_key_t key)

ThreadLocalPtr类

有时候,我们并不想要各个线程独立的变量,我们仍然需要一个全局变量,线程局部变量只是作为全局变量的缓存,用以缓解并发。在RocksDB中ThreadLocalPtr这个类就是来干这个事情的。ThreadLocalPtr类包含三个内部类,ThreadLocalPtr::StaticMeta,ThreadLocalPtr::ThreadData和ThreadLocalPtr::Entry。其中StaticMeta是一个单例,管理所有的ThreadLocalPtr对象,我们可以简单认为一个ThreadLocalPtr对象,就是一个线程局部存储(ThreadLocalStorage)。但实际上,全局我们只定义了一个线程局部变量,从StaticMeta构造函数可见一斑。那么全局需要多个线程局部缓存怎么办,实际上是在局部存储空间做文章,线程局部变量实际存储的是ThreadData对象的指针,而ThreadData里面包含一个数组,每个ThreadLocalPtr对象有一个独立的id,在其中占有一个独立空间。获取某个变量局部缓存时,传入分配的id即可,每个Entry中ptr指针就是对应变量的指针。

ThreadLocalPtr::StaticMeta::StaticMeta() : next_instance_id_(0), head_(this) {if (pthread_key_create(&pthread_key_, &OnThreadExit) != 0) {abort();}......
}void* ThreadLocalPtr::StaticMeta::Get(uint32_t id) const {auto* tls = GetThreadLocal();return tls->entries[id].ptr.load(std::memory_order_acquire);
}struct Entry {Entry() : ptr(nullptr) {}Entry(const Entry& e) : ptr(e.ptr.load(std::memory_order_relaxed)) {}std::atomic<void*> ptr;
};

整体结构如下:每个线程有一个线程局部变量ThreadData,里面包含了一组ThreadLocalPtr的指针,对应的是多个变量,同时ThreadData之间相互通过指针串联起来,这个非常重要,因为执行写操作时,写线程需要修改所有thread的局部缓存值来通知共享变量发生变化了。

 ---------------------------------------------------|          | instance 1 | instance 2 | instnace 3 |---------------------------------------------------| thread 1 |    void*   |    void*   |    void*   | <- ThreadData---------------------------------------------------| thread 2 |    void*   |    void*   |    void*   | <- ThreadData---------------------------------------------------| thread 3 |    void*   |    void*   |    void*   | <- ThreadDatastruct ThreadData {explicit ThreadData(ThreadLocalPtr::StaticMeta* _inst): entries(), inst(_inst) {}std::vector<Entry> entries;ThreadData* next;ThreadData* prev;ThreadLocalPtr::StaticMeta* inst;
};

读写无并发冲突

现在说到最核心的问题,我们如何实现利用TLS来实现本地局部缓存,做到读不上锁,读写无并发冲突。读、写逻辑和并发控制主要通过ThreadLocalPtr中通过3个关键接口Swap,CompareAndSwap和Scrape实现。对于ThreadLocalPtr< Type* > 变量来说,在具体的线程局部存储中,会保存3中不同类型的值:

1). 正常的Type* 类型指针;

2). 一个Type*类型的Dummy变量,记为InUse;

3). nullptr值,记为obsolote;

读线程通过Swap接口来获取变量内容,写线程则通过Scrape接口,遍历并重置所有ThreadData为(obsolote)nullptr,达到通知其他线程局部缓存失效的目的。下次读线程再读取时,发现获取的指针为nullptr,就需要重新构造局部缓存。

//获取某个id对应的局部缓存内容,每个ThreadLocalPtr对象有单独一个id,通过单例StaticMeta对象管理。
void* ThreadLocalPtr::StaticMeta::Swap(uint32_t id, void* ptr) {
//获取本地局部缓存
auto* tls = GetThreadLocal();                                        return tls->entries[id].ptr.exchange(ptr, std::memory_order_acquire);
}bool ThreadLocalPtr::StaticMeta::CompareAndSwap(uint32_t id, void* ptr,void*& expected) {//获取本地局部缓存auto* tls = GetThreadLocal();return tls->entries[id].ptr.compare_exchange_strong(expected, ptr, std::memory_order_release, std::memory_order_relaxed);
}//将所有管理的对象指针设置为nullptr,将过期的指针返回,供上层释放,
//下次进行从局部线程栈获取时,发现内容为nullptr,则重新申请对象。
void ThreadLocalPtr::StaticMeta::Scrape(uint32_t id, std::vector<void*>* ptrs, void* const replacement) {                           MutexLock l(Mutex());for (ThreadData* t = head_.next; t != &head_; t = t->next) {                               if (id < t->entries.size()) {                                                            void* ptr =t->entries[id].ptr.exchange(replacement, std::memory_order_acquire);               if (ptr != nullptr) {//搜集各个线程缓存,进行解引用,必要时释放内存ptrs->push_back(ptr);}                                                                            }}
}//初始化,或者被替换为nullptr后,说明缓存对象已经过期,需要重新申请。
ThreadData* ThreadLocalPtr::StaticMeta::GetThreadLocal() {申请线程局部的ThreadData对象,通过StaticMeta对象管理成一个双向链表,每个instance对象管理一组线程局部对象。if (UNLIKELY(tls_ == nullptr)) {auto* inst = Instance();tls_ = new ThreadData(inst);{                                                                        // Register it in the global chain, needs to be done before thread exit// handler registration
      MutexLock l(Mutex());                                                  inst->AddThreadData(tls_);}return tls_;                                             }
}

读操作包括两部分,Get和Release,这里面除了从TLS中获取缓存,还涉及到一个释放旧对象内存的问题。Get时,利用InUse对象替换TLS对象,Release时再将TLS对象替换回去,读写没有并发的场景比较简单,如下图,其中TLS Object代表本地线程局部缓存,GlobalObject是全局共享变量,对所有线程可见。

下面我们再看看读写有并发的场景,读线程读到TLS object后,写线程修改了全局对象,并且遍历对所有的TLS object进行修改,设置nullptr。在此之后,读线程进行Release时,compareAndSwap失败,感知到使用的object已经过期,执行解引用,必要时释放内存。当下次再次Get object时,发现TLS object为nullptr,就会使用当前最新的object,并在使用完成后,Release阶段将object填回到TLS。

应用场景

从前面的分析来看,TLS作为cache,仍然需要一个全局变量,全局变量保持最新值,而TLS则可能存在滞后,这就要求我们的使用场景不要求读写要实时严格一致,或者能容忍多版本。全局变量和局部缓存有交互,交互逻辑是,全局变量变化后,局部线程要能及时感知到,但不需要实时。允许读写并发,即允许读的时候,使用旧值读,待下次读的时候,再获取到新值。Rocksdb中的superversion管理则符合这种使用场景,swich/flush/compaction会产生新的superversion,读写数据时,则需要读supversion。往往读写等前台操作相对于switch/flush/compaction更频繁,所以读superversion比写superversion比例更高,而且允许系统中同时存留多个superversion。

每个线程可以拿superversion进行读写,若此时并发有flush/compaction产生,会导致superversion发生变化,只要后续再次读取superversion时,能获取到最新即可。细节上来说,扩展到应用场景,一般在读场景下,我们需要获取snapshot,并借助superversion信息来确认这次读取要读哪些物理介质(mem,imm,L0,L1...LN)。

1).获取snapshot后,拿superversion之前,其它线程做了flush/compaction导致superversion变化

这种情况下,可以拿到最新的superversion。

2).获取snapshot后,拿superversion之后,其它线程做了flush/compaction导致superversion变化

这种情况下,虽然superversion比较旧,但是依然包含了所有snapshot需要的数据。那么为什么需要及时获取最新的superversion,这里主要是为了回收废弃的sst文件和memtable,提高内存和存储空间利用率。

总结

RocksDB的线程局部缓存是一个很不错的实现,用户使用局部缓存可以大大降低读写并发冲突,尤其在读远大于写的场景下,整个缓存维护代价也比较低,只有写操作时才需要锁保护。只要系统中允许共享变量的多版本存在,并且不要求实时保证一致,那么线程局部缓存是提升并发性能的一个不错的选择。

转载于:https://www.cnblogs.com/cchust/p/11562949.html

RocksDB线程局部缓存相关推荐

  1. django中的缓存 单页面缓存,局部缓存,全站缓存 跨域问题的解决

    django中的缓存 单页面缓存,局部缓存,全站缓存 跨域问题的解决 参考文章: (1)django中的缓存 单页面缓存,局部缓存,全站缓存 跨域问题的解决 (2)https://www.cnblog ...

  2. 并发基础(十) 线程局部副本ThreadLocal之正解

    什么是ThreadLocal ThreadLocal是线程局部变量,所谓的线程局部变量,就是仅仅只能被本线程访问,不能在线程之间进行共享访问的变量.在各个Java web的各种框架中ThreadLoc ...

  3. 详解CPU的主频、核心、线程、缓存、架构

    CPU的重要参数:主频.核心.线程.缓存.架构. 1.主频: 在CPU的参数里的 3.0GHz.3.7GHz等就是CPU的主频,严谨的说他是CPU内核的时钟频率,可直接理解为运算速度. CPU的主频相 ...

  4. 计算机处理器缓存参数,小知识:通俗易懂理解处理器中的主频、架构、核心、线程、缓存...

    小知识:通俗易懂理解处理器中的主频.架构.核心.线程.缓存 处理器,作为计算机的核心硬件,决定了一台电脑的运算性能好坏.我们在选择处理器的时候,通常都会在网上查询处理器型号参数,主要是看主频.架构.核 ...

  5. 02-CPU基础知识:通俗易懂方式理解主频、核心、线程、缓存、架构

    我们通常会将CPU比喻为人类的大脑,是计算机的核心硬件,决定了一台电脑的运算性能好坏.我们在选购CPU的时候,通常都会在网上查询处理器型号参数,主要是看主频.核心.线程.缓存.架构等参数,那么对于小白 ...

  6. spring+ehcache实现页面整体缓存和页面局部缓存

    第一步:首先配置ehcache.xml指定我们的SimplePageCachingFilter缓存  ,这里指定页面缓存的生命周期是60秒,还有timeToIdleSeconds的时间爱你是120秒, ...

  7. 海康工业相机SDK + OpenCV实例(5):相机双线程读写缓存策略

    海康工业相机SDK + OpenCV实例(5): 相机双线程读写缓存策略 文章目录 海康工业相机SDK + OpenCV实例(5): 相机双线程读写缓存策略 前言 一.双线程 二.缓存区容量为2的生产 ...

  8. CPU知识:主频、核心、线程、缓存、架构

    我们都说CPU相当于人类的大脑,在日常生活中,人脑是术业有专攻,有人天生适合搞艺术,有人天生适合搞科学.CPU作为计算机的大脑,其实也是这样的.下面就带大家了解一下CPU知识以及怎么选择合适的CPU. ...

  9. 计算机知识科普讲解,计算机组成原理-CPU-CPU知识科普:秒懂主频、核心、线程、缓存、架构详解...

    我们都说CPU相当于人类的大脑,在日常生活中,人脑是术业有专攻,有人天生适合搞艺术,有人天生适合搞科学.CPU作为计算机的大脑,其实也是这样的.下面就带大家了解一下CPU知识以及怎么选择合适的CPU. ...

最新文章

  1. 【组队学习】【31期】数据可视化(Matplotlib)
  2. linux系统查看CPU使用含义、IO、内存、硬盘使用、负载
  3. php验证中文姓名,我想在表单验证中加入中文姓名合法性模糊匹配判断?
  4. token和session的区别
  5. 全球500强企业人力资源管理之道
  6. 联想gen系列服务器,Hpe Microserver Gen10 Plus开箱
  7. 如果能够让出资人了解更多的c++项目进程
  8. 达索系统成立“大土木工程达索系统BIM技术推进联盟”深化应用、共享经验
  9. ArcGIS API for JavaScript实现分图层显示
  10. 微网站|h5弹窗|手机网站 html5 弹窗、弹层、提示框、加载条
  11. json html双引号,当gethtml方法返回json时,json中的字符串如果存在双引号,会破坏json的格式, 如:quot;...
  12. Integer在webservice的传递
  13. matlab练习程序(倾斜校正,透视变换)
  14. 临床大数据分析与挖掘
  15. matlab实现拉格朗日插值法后的实验感想
  16. 【Codeforces】A1组刷题记录(50/ 50)完结
  17. 用英文字母解析漩涡鸣人
  18. 杭电ACM 2000-2099 100道题 详细解题报告出炉
  19. hadoop+Spark实战基于大数据技术之电视收视率企业项目实战
  20. 实践丨分布式事务解决方案汇总:2PC、消息中间件、TCC、状态机+重试+幂等

热门文章

  1. group by是否会用到索引_平时工作中经常用到的SQL,这些你都知道吗?
  2. php 常见的算法题,php最常见最经典的算法题(1)
  3. 算法与数据结构(面向对象思想)
  4. 浅析如何让企业网站实现精准营销?
  5. solrcloud java_SolrCloud之zookeeper中使用java代码创建集合
  6. phython在file同时写入两个_喜大普奔,两个开源的 Spring Boot + Vue 前后端分离项目可以在线体验了
  7. 华为硬件笔试 通用器件知识2_华为硬件笔试题(最新版)
  8. java导出模板 pdf设置字体_有哪些相见恨晚的PPT模板网站?
  9. 趣谈网络协议笔记-零
  10. android studio下载插件时出现的read time out问题