Envoy源码分析之ThreadLocal

2023-11-16

ThreadLocal机制

​ Envoy中的ThreadLocal机制其实就是我们经常说的线程本地存储简称TLS(Thread Local Storage),顾名思义通过TLS定义的变量会在每一个线程专有的存储区域存储一份,访问TLS的时候,其实访问的是当前线程占有存储区域中的副本,因此可以使得线程可以无锁的并发访问同一个变量。Linux上一般有三种方式来定义一个TLS变量。

  • gcc对C语言的扩展__thread
  • pthread库提供的pthread_key_create
  • C++11的std::thread_local关键字

​ Envoy的ThreadLocal机制就是在C++11的std::thread_local基础上进行了封装用于实现线程间的数据共享。Envoy因其配置的动态生效而出名,而配置动态生效的基石就是ThreadLocal机制,通过ThreadLocal机制将配置可以无锁的在多个线程之间共享,当配置发生变更的时候,通过主线程将更新后的配置Post到各个线程中,交由各个线程来更新自己的ThreadLocal

ThreadLocalObject

​ Envoy要求所有的ThreadLocal数据对象都要继承ThreadLocalObject,比如下面这个ThreadLocal对象。

struct ThreadLocalCachedDate : public ThreadLocal::ThreadLocalObject {
   ThreadLocalCachedDate(const std::string& date_string) : 
   date_string_(date_string) {}
  const std::string date_string_;
};

​ 但实际上ThreadLocalObject只是一个空的接口类,所以并非我们继承了ThreadLocalObject就是一个TLS了。继承ThreadLocalObject目的是为了可以统一对所有要进行TLS的对象进行管理。

class ThreadLocalObject {
public:
  virtual ~ThreadLocalObject() = default;
};
using ThreadLocalObjectSharedPtr = std::shared_ptr<ThreadLocalObject>;

​ Envoy中需要TLS的数据有很多,最重要的当属配置,随着配置的增多,这类数据所占据的内存也会变得很大,如果每一种配置都声明为TLS会导致不少内存浪费。为此Envoy通过ThreadLocalData将所有要进行TLS的对象都管理起来,然后将ThreadLocalData本身设置为TLS,通过TLS中保存的指针来访问对应的数据。这样就可以避免直接在TLS中保存数据而带来内存上的浪费,只需要保存指向数据的指针即可,相关代码如下。

struct ThreadLocalData {
  // 指向当前线程的Dispatcher对象 
  Event::Dispatcher* dispatcher_{};
  // 保存了所有要TLS的数据对象的智能指针,通过智能指针来访问真正的数据对象
  std::vector<ThreadLocalObjectSharedPtr> data_;
};

4-2.jpg

如上图所示,每一个TLS通过指针指向实际的对象,每一个数据对象只在内存中保存一份,避免内存上的浪费,但是这样带来问题就是如何做到线程安全的访问数据对象呢? 当我们要访问数据对象的时候,如果此时正在对数据对象进行更新,这个时候就会存在一个线程安全的问题了。Envoy巧妙的通过在数据对象更新的时候,先构造出一个新的数据对象,然后将TLS中的数据对象指针指向新的数据对象来实现线程安全的访问。本质上和COW(copy-on-write)很类似,但是存在两点区别。

  • COW中是先拷贝原来的对象,然后更改对象,而Envoy在这里是重新构建一个新的数据对象
  • COW中无论是读还是写,在更改shared_ptr指向时,都需要加锁,因为shared_ptr本身的读写时非线程安全的,而Envoy不需要加锁。

​ Envoy中指向数据对象的shared_ptr并非只有一个,而是每一个线程都有一个shared_ptr指向数据对象,更改shared_ptr指向新的数据对象时通过post一个任务到对应线程中,然后在同一个线程使shared_ptr指向新的数据对象,因此并没有多线程操作shared_ptr,所以没有线程安全问题,自然也不用加锁,这是Envoy实现比较巧妙的地方。

4-3.jpg

​ 如上图所示,T1时刻,Thread1通过TLS对象访问ThreadLocalObjectOld,在T2时刻在main线程发现配置发生了变化,重新构造了一个新的ThreadlocalObjectNew对象,然后通过Thread1的Dispatcher对象post了一个任务到Thread1线程,到了T3时刻这个任务开始执行,将对应的指针指向了 ThreadLocalObjectNew,最后在T4时刻再次访问配置的时候,就已经访问的是最新的配置了。到此为止就完成了一次配置更新,而且整个过程是线程安全的。

ThreadLocal

​ 终于到了分析真正的ThreadLocal对象的时候,它的功能其实很简单,大部分的能力都是依赖Dispatcher、还有上文中提到的SlotImplThreadLocalData等,Instance是它的接口类,它继承了SlotAllocator接口,也包含了上文中分析的allocateSlot方法。

class Instance : public SlotAllocator {
public:
  // 每启动一个worker线程就需要通过这个方法进行注册
  virtual void registerThread(Event::Dispatcher& dispatcher, bool main_thread) PURE;
  // 主线程在退出的时候调用,用于标记shutdown状态
  virtual void shutdownGlobalThreading() PURE;
  // 每一个worker线程需要调用这个方法来释放自己的TLS
  virtual void shutdownThread() PURE;
  virtual Event::Dispatcher& dispatcher() PURE;
};

​ 对应的实现是InstanceImpl对象,在Instance 的基础上又扩展了一些post任务到所有线程的一些方法。


class InstanceImpl : public Instance {
 public:
	....
 private:
  // post任务到所有注册的线程中
  void runOnAllThreads(Event::PostCb cb);
  // post任务到所有注册的线程中,完成后通过main_callback进行通知
  void runOnAllThreads(Event::PostCb cb, Event::PostCb main_callback);
  // 初始化TLS指向对应的数据对象指针
  static void setThreadLocal(uint32_t index, ThreadLocalObjectSharedPtr object);
  .....
  // 保存所有注册的线程
  std::list<std::reference_wrapper<Event::Dispatcher>> registered_threads_;

​ 因为所有的线程都会注册都InstanceImpl中,所以只需要遍历所有的线程所对应的Dispatcher 对象,调用其post方法将任务投递到对应线程即可,但是如何做到等所有任务执行完成后进行通知呢 ?

void InstanceImpl::runOnAllThreads(Event::PostCb cb, 
                                   Event::PostCb all_threads_complete_cb) {
  ASSERT(std::this_thread::get_id() == main_thread_id_);
  ASSERT(!shutdown_);
  // 首先在主线程执行任务
  cb();
  // 利用了shared_ptr自定义析构函数,在析构的时候向主线程post一个完成的通知任务
  // 这个机制和Bookkeeper的实现机制是一样的。
  std::shared_ptr<Event::PostCb> cb_guard(new Event::PostCb(cb),
                   [this, all_threads_complete_cb](Event::PostCb* cb) {
                    main_thread_dispatcher_->post(all_threads_complete_cb);
                      delete cb; });

  for (Event::Dispatcher& dispatcher : registered_threads_) {
    dispatcher.post([cb_guard]() -> void { (*cb_guard)(); });
  }
}

​ 通过上面的代码可以看到,这里仍然利用到了shared_ptr的引用计数机制来实现的。每一个post到其他线程的任务都会导致cb_guard引用计数加1,post任务执行完成后cb_guard引用计数减1,等全部任务完成后,cb_guard 的引用计数就变成0了,这个时候就会执行自定义的删除器,在删除器中就会post一个任务到主线程中,从而实现了任务执行完成的通知回调机制。

​ 接下来我们来分析下shutdownGlobalThreading,这个函数是用于设置flag来表示正在关闭TLS,必须由主线程在其它worker线程退出之前来调用,调用完成后每一个worker线程还需要调用对应TLS的shutdownThread来清理TLS中的对象,到此为止才完成了全部的TLS清理工作。

void InstanceImpl::shutdownGlobalThreading() {
  ASSERT(std::this_thread::get_id() == main_thread_id_);
  ASSERT(!shutdown_);
  shutdown_ = true;
}

上面的代码是shutdownGlobalThreading的实现,可以看到仅仅是设置了一个shutdown_的标志。

​ 最后来分析一下shutdownThread,每一个work线程在退出事都需要调用这个函数,这个函数会将存储的所有线程存储的对象进行清除。每一个worker线程都持有InstanceImpl实例的引用,在析构的时候会调用shutdownThread来释放自己线程的TLS内容,这个函数的实现如下:

void InstanceImpl::shutdownThread() {
  ASSERT(shutdown_);
  for (auto it = thread_local_data_.data_.rbegin(); 
	   it != thread_local_data_.data_.rend(); ++it) {
    it->reset();
  }
  thread_local_data_.data_.clear();
}

​ 比较奇怪的点在于这里是逆序遍历所有的ThreadLocalObject对象来进行reset的,这是因为一些"持久"(活的比较长)的对象如ClusterManagerImpl很早就会创建ThreadLocalObject对象,但是直到shutdown的时候也不析构,而在此基础上依赖ClusterManagerImpl的对象的如GrpcClientImpl等,则是后创建ThreadLocalObject对象,如果ClusterManagerImpl创建的ThreadLocalObject对象先析构,而GrpcClientImpl相关的ThreadLocalObject对象依赖了ClusterManagerImpl相关的TLS内容,那么后析构就会导致未定义的问题。为此这里选择逆序来进行reset,先从一个高层的对象开始,最后才开始对一些基础的对象所关联的ThreadLocalObject进行reset。例如下面这个例子:

struct ThreadLocalPool : public ThreadLocal::ThreadLocalObject {
	.....
  InstanceImpl& parent_;
  Event::Dispatcher& dispatcher_;
  Upstream::ThreadLocalCluster* cluster_;
	.....
};

redis_proxy中定义了一个ThreadLocalPool,这个ThreadLocalPool又依赖较为基础的ThreadLocalCluster(是ThreadLocalClusterManagerImpl的数据成员,也就是ClusterManagerImpl所对应的ThreadLocalObject对象),如果shutdownThread按照顺序的方式析构的话,那么ThreadLocalPool中使用的ThreadLocalCluster会先被析构,然后才是ThreadLocalPool的析构,而ThreadLocalPool析构的时候又会使用到ThreadLocalCluster,但是ThreadLocalCluster已经析构了,这个时候就会出现野指针的问题了。

ThreadLocalPool::ThreadLocalPool(InstanceImpl& parent, 
                                 Event::Dispatcher& dispatcher, const 
                                 std::string& cluster_name)
    : parent_(parent), dispatcher_(dispatcher), 
	cluster_(parent_.cm_.get(cluster_name)) {
  .....
  local_host_set_member_update_cb_handle_ = 
  cluster_->prioritySet().addMemberUpdateCb(
      [this](uint32_t, const std::vector<Upstream::HostSharedPtr>&,
             const std::vector<Upstream::HostSharedPtr>& hosts_removed) -> void {
        onHostsRemoved(hosts_removed);
      });
}

ThreadLocalPool::~ThreadLocalPool() {
  // local_host_set_member_update_cb_handle_是ThreadLocalCluster的一部分
  // ThreadLocalCluster析构会导致local_host_set_member_update_cb_handle_变成野指针
  local_host_set_member_update_cb_handle_->remove();
  while (!client_map_.empty()) {
    client_map_.begin()->second->redis_client_->close();
  }
}

​ 到此为止关于Envoy中的TLS实现就全部分析完毕了。

小结

​ 通过本节的分析相信我们应该足以驾驭Envoy中的ThreadLocal,从其设计可以看出它的一些其巧妙之处,比如抽象出一个Slot和对应的线程存储进行了关联,Slot可以任意传递,因为不包含实际的数据,拷贝的开销很低,只包含了一个索引值,具体关联的线程存储数据是不知道的,避免直接暴露给用户背后的数据。而InstanceImpl对象则管理着所有Slot的分配和移除以及整个ThreadLocal对象的shutdown。还有引入的Bookkeeper机制也甚是巧妙,和Envoy源码分析之Dispatcher机制一文中的DeferredDeletable机制有着异曲同工之妙,通过这个机制可以做到安全的析构SlotImpl对象

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Envoy源码分析之ThreadLocal 的相关文章

  • 官方YOLOV5的torch模型->ONNX模型->RKNN模型

    1 环境配置 1 1 RKNN Toolkit2的环境配置 下载RKNN Toolkit2 git clone https github com rockchip linux rknn toolkit2 git 打开一个终端命令行窗口 安装
  • C++primer(第五版)---14章(重载运算与类型转换)

    目录 编辑 重载运算符的基本概念 调用重载运算符 而有些运算符不应该被重载 应该保持与内置类型一致的含义 是否成员成员函数 重载io lt lt 和 gt gt 运算符 重载 lt lt 运算符 重载 gt gt 运算符 算术运算符 关系运
  • python英文(无空格)文本分词模块wordninja使用

    在NLP中 数据清洗与分词往往是很多工作开始的第一步 大多数工作中只有中文语料数据需要进行分词 现有的分词工具也已经有了很多了 这里就不再多介绍了 英文语料由于其本身存在空格符所以无需跟中文语料同样处理 如果英文数据中没有了空格 那么应该怎
  • react在移动端的自适应布局

    1 移动端基本可以无阻碍的进行flex的弹性布局 这边对flex就不进行深究 2 可以进行依据窗口进行vw vh vmin vmax单位的布局 1 vw vh vmin vmax 是一种视窗单位 也是相对单位 它相对的不是父节点或者页面的根
  • Android USB Camera(1) : 调试记录

    1 前言 前段时间调试了一个uvc摄像头 这里做下记录 硬件平台为mt6735 软件平台为android 5 0 2 底层配置 UVC全称是usb video class 一种usb视频规范 所有遵循uvc协议的摄像头都不需要安装额外的驱动
  • integer conversion resulted in a change of sign

    Type 69 D integer conversion resulted in a change of sign MDK 出现 68 D integer conversion resulted in a change of sign 在K
  • 三行代码实时追踪你的手,只要有浏览器就够了

    栗子 发自 凹非寺 量子位 报道 公众号 QbitAI Are You OK O K 人脸不管做了多么一言难尽的表情 五官也不太会四处乱跑 手就不一样了 手势百媚千娇 镜头看到的画面就百媚千娇 所以 AI怎么识别手呢 一位叫做Victor
  • Java获取月份天数错误

    之前编写获取日期函数如下 获取某年某月有多少天 return 该月的天数 public static int getDaysAboutMonth int year int month Calendar c Calendar getInsta
  • 电力电子转战数字IC——我的IC面试(2022.10.14更新)

    目录 感谢信 HKWS10 14面试 25mins JXC10 13面试 30mins JDSK9 23面试 42mins 快速的自我介绍 介绍一下这个MCDF的项目 你这里写SV搭建的验证环境 和UVM搭建的有什么区别吗 你这里写了覆盖率
  • 正视周期,创业在衰退中砥砺前行

    比衰退更重要的是 早期投资机构正在面临结构性机遇 数科星球原创 作者丨苑晶 编辑丨大兔 周期 犹如一只隐形的手 在2023年影响着芸芸众生 从经济周期 行业周期再到货币周期 这个隐形的手牵动着消费者需求 产业链变迁 政策变动等多种因素 亦牵
  • 12.SpringBoot整合mybatis实现插入操作

    本文基于10 springboot整合mybatis环境 默认环境配置好的 下面进入正题 首先查看以下数据库表有什么内容 然后 在EmpMapper中定义插入方法 并在emp xml中加入insert语句 这里推荐使用navicat复制其中
  • 概率图论:了解概率分布、概率独立性和随机化

    作者 禅与计算机程序设计艺术 概率图模型 Probabilistic Graphical Model PGM 是现代统计学习中的一个重要工具 它通过描述变量间的依赖关系和概率分布来对复杂系统进行建模 概率图模型由两部分组成 一是概率模型 它
  • QPoint与QPointF的区别

    QPointF类使用浮点精度定义平面中的点 QPoint类使用整数精度定义平面中的点

随机推荐

  • C语言链表嵌套链表学生成绩管理系统

    一阶段考核标准 用C语言链表嵌套链表学生成绩管理系统 链表A 每个节点存放一个新的链表B1 B2 B3 B4 B5的头结点 场景 一个年级 相当链表A 该年级5个班 每个班5个人 相当于链表B1 B5 做一个学生成绩管理系统学生成绩有语文
  • java版工程管理系统Spring Cloud+Spring Boot+Mybatis实现工程管理系统源码

    工程项目管理软件 工程项目管理系统 对建设工程项目管理组织建设 项目策划决策 规划设计 施工建设到竣工交付 总结评估 运维运营 全过程 全方位的对项目进行综合管理 工程项目各模块及其功能点清单 一 系统管理 1 数据字典 实现对数据字典标签
  • Java中transient关键字的详细总结

    目录 一 概要介绍 1 序列化 2 为什么要用transient关键字 3 transient的作用 二 transient使用总结 三 使用场景 一 概要介绍 本文要介绍的是Java中的transient关键字 transient是短暂的
  • the left operand of ** is a garbage value

    CapLocation location 0 if segmentedControl tag SegmentControlTag if segmentIndex 0 location CapLeft else if segmentIndex
  • 最全的前端性能优化手段回答

    前端性能优化手段 参考答案 前端性能优化手段从以下几个方面入手 加载优化 执行优化 渲染优化 样式优化 脚本优化 1 加载优化 减少HTTP请求 缓存资源 压缩代码 无阻塞 首屏加载 按需加载 预加载 压缩图像 减少Cookie 避免重定向
  • 时序预测模型汇总

    时序预测模型 一 自回归 AR 在 AR 模型中 我们使用变量过去值的线性组合来预测感兴趣的变量 术语自回归表明它是变量对自身的回归 二 移动平均模型 MA 与在回归中使用预测变量的过去值的 AR 模型不同 MA 模型在类似回归的模型中关注
  • 三角函数的向量表示的原理计算

    在 电路 中 三相电源经常用复数或者是向量来表示 但是与我们初高中熟知的空间向量不同 这里的三相交流电是一种时间向量 由于采用的形式是正弦形式 使得其也可以用空间向量中的平行四边形原则来进行计算合成 下面将介绍一下正弦量可以用向量表示的原理
  • 生成tensorrt引擎错误记录-yolov5

    warning nvinfer1 Dims type is deprecated Wdeprecated declarations note TRT DEPRECATED DimensionType type MAX DIMS lt The
  • c++ virtual 关键字 override 关键字

    文章目录 1 什么是virtual 2 为什么需要 3 通常用在什么情形 4 延伸 虚函数 纯虚函数 override 关键字 9 问题汇总 9 1 非虚函数和虚函数都可以重写 那区别是啥 9 2 基类虚函数 纯虚函数 子类有没有 over
  • MS5543单通道、16位、串行通信、高速ADC转换芯片

    产品简述 MS5543 是一款单通道 16 位 串行输入 电压输出的数模 转换器 采用 2 7V 至 5 5V 单电源供电 输出范围为 0V 至 V REF 在输出范围内保证单调性 在温度范围为 40 C 至 85 C 能够提供 1LSB
  • linux设备驱动归纳总结(四):3.抢占和上下文切换

    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 上一节介绍了进程调度的一些基本概念 并简单介绍了在没有抢占的情况下
  • 生产环境使用HBase,你必须知道的最佳实践

    需要关注的一些最佳实践经验 Schema设计七大原则 1 每个region的大小应该控制在10G到50G之间 2 一个表最好保持在 50到100个 region的规模 3 每个cell最大不应该超过10MB 如果超过 应该有些考虑业务拆分
  • Java中存储金额用什么数据类型

    文章目录 1 抛砖引玉 2 加减乘除 3 大小比较 4 小数位数及四舍五入规则 1 抛砖引玉 在给自己做一个小的Java记账小程序的时候 对金额的处理时必不可少的 一开始选择的是float数据类型 在数据库中 存储金额的数据字段也是floa
  • 二进制转16进制字符串和16进制字符串转二进制的C和JAVA实现

    二进制转16进制字符串和16进制字符串转二进制的C语言实现 二进制转16进制字符串 长度会翻倍 void ByteToHexStr const unsigned char source char dest int sourceLen 16进
  • 《Kubernetes部署篇:Ubuntu20.04基于外部etcd+部署kubernetes1.24.17集群(多主多从)》

    一 部署架构图 1 架构图如下所示 2 部署流程图如下所示 二 环境信息 1 部署规划 主机名 K8S版本 系统版本 内核版本 IP地址 备注 k8s master 63 1 24 17 Ubuntu 20 04 5 LTS 5 15 0
  • 【QT】判断鼠标按键

    代表按键类型的枚举变量 enum Qt MouseButton Qt NoButton 0x00000000 Qt AllButtons 0x07ffffff Qt LeftButton 0x00000001 Qt RightButton
  • 12306验证码识别 --- 2017-12

    1 附件中包含12306查询验证码识别客户端和所需要测试的样本图片 2 模型正确率95 以上 3 操作方式 3 1 解压里面的Client zip 找到里面的user client exe可执行文件 3 2 点击里面的browse按钮进行选
  • unity基本知识点2

    一 把图片打包成图集 1 全选图片 把texture type改成sprite 2DandUI Packing Tag是打包标签 给想打包的图片写上统一的标签才可打包 2 edit project setting editor里inspec
  • 测试人:“躺平?不可能的“, 盘点测试人在职场的优势

    之前有这么一个段子 有人喜欢创造世界 他们做了程序员 有人喜欢拯救世界 他们做了测试员 近几年 测试工程师在企业究竟是怎么样的发展 随着企业对于用户体验的满意度越来越重视 更加推动了软件测试工程师这个岗位的需求度 接下来 我们从4个纬度来分
  • Envoy源码分析之ThreadLocal

    ThreadLocal机制 Envoy中的ThreadLocal机制其实就是我们经常说的线程本地存储简称TLS Thread Local Storage 顾名思义通过TLS定义的变量会在每一个线程专有的存储区域存储一份 访问TLS的时候 其