前言
近期线程局部存储thread_local的应用上,暴露出了不少问题,本文从概念到典型的案例分析,可以加深开发人员对thread_local的理解,有助于规避同类问题的发生。
线程局部存储
在Linux中有一种线程局部存储方法,就是使用关键字__thread来定义变量。__thread是GCC内置的线程局部存储设施(Thread-Local Storage),凡是带有__thread的变量,每个线程都拥有该变量的一份拷贝,且互不干扰。线程局部存储中的变量将一直存在,直至线程终止,当线程终止时会自动释放这一存储。__thread并不是所有数据类型都可以使用的,因为其只支持POD类型,不支持class类型——其不能自动调用构造函数和析构函数。同时__thread可以用于修饰全局变量、函数内的静态变量,但是不能用于修饰函数的局部变量或者class的普通成员变量。
thread_local是C++11引入的新特性,是一个关键字,用于修饰变量(thread_local关键字和static、extern关键字在使用上是不冲突的),告诉线程对此关键词修饰的数据进行本地存储。
thread_local的使用
thread_local主要是用来修饰变量,修饰变量的种类有以下四种
- 1)全局变量
- 2)局部变量
- 3)类对象
- 4)类成员变量
根据各个模块使用的情况,可以大致分为以下风险点。
1.线程大量的申请与释放可能引起不合适的接口调用
报盘插件中维护了线程局部变量“消息写入封装对象”,由于新创建线程会导致重新申请一份对象,那么就会调用一次框架函数,但是这个函数不建议频繁的申请,应该是担心内存碎片的大量产生。
2.用户调用流程不合理可能得到非预期结果
报盘框架对业务提供了打点的功能,业务需要在进行打点时,先调用获取报盘下行上下文接口,但是打点结束是在报盘插件中进行的,这样话,必须强制与业务做好限制。在下面情况下可能出现风险。
业务人员不熟悉流程,调用了调用获取报盘下行上下文接口,而未执行报盘插件中发送消息的接口,导致内存泄漏。
业务人员调用了获取报盘下行上下文接口,但是在执行业务函数时因为报错,函数直接返回,并未执行报盘插件中发送消息的接口,导致内存泄漏。
业务人员也有可能只执行了报盘插件中发送消息的接口,导致报盘下行上下文未初始化。
个人建议:发送消息的函数里不要包含打点结束的功能,另外对用户使用的错误,有日志输出。
3.线程局部变量与线程栈内存不合理的搭配使用
这种问题类似返回函数栈空间的引用或者指针,都会出现无法的地址访问。
4. 不合时宜的dlclose库句柄
当某个动态库包含thread_local自定义类变量时,调用者如果在存在thread_local变量的线程未结束前,就dlclose库句柄,那么当线程退出时会发生崩溃。这是因为dlcose后,库被卸载(对应的虚拟地址也不存在了),当调用者里的线程结束时,线程局部变量副本会自动调用析构函数,这时候访问不存在的地址,那么就会崩溃。当把线程局部变量改成指针类型后,便不会出现这个问题,这是因为不需要调用析构函数。
当业务库使用线程局部存储变量时需要小心,调用者需要做好流程控制。
5.同线程的跨模块的递归调用,可能影响你的业务处理
对外提供的函数中包含了thread_local变量,你无法预测外界如何调用它,需要考虑当前线程是否已经存在了被使用过的thread_local变量,因为你可能调用多次这个变量对象的函数了,它会使得逻辑出错。
6.dlclose() 不调用全局对象的析构函数
(静态)全局变量尽量不用使用!!!!他在dlclose库时,析构的顺序有可能是不可预测的,这在跨平台开发时是有风险的。特别是搭配thread_local变量时,线程中的线程局部存储(副本)在随着线程消亡而需要资源释放资源时,遇到4的情况,就会出现崩溃。
总结
用thread_local的意义,简化代码,声明一次,但是使得每个线程启动时使用独有一份副本,不用每个线程执行时再通过代码管理这份资源。当我们(实现)维护代码时,发现不同的线程都想使用同一个变量,又不想使用锁,使用它再合适不过了。但是在跨库使用时,需要做好资源管理,避免使用全局静态变量,提供优雅的资源释放接口,不依赖系统的自动释放规则。