线程局部存储(TLS)

线程局部存储介绍

Linux下线程的数据段和代码段共享,栈私有。有的时候我们想使用一个全局数据,但是全局数据都是在数据段的,它没办法每个线程独享,所以这个时候就有了线程局部存储的出现。
举个列子,我们写一个服务端程序,每个线程都有一个自己的配置,我们不想每次都在使用的时候从栈上创建这样太浪费时间,那么我们只能创建一个全局的,这个时候全局的数据又不能独享,那么这个场景下我们可以使用线程局部存储,它是一个全局数据但是每个线程私有。

线程局部存储方式
  1. gcc 下的 __thread 关键字,它是个存储型修饰符,它可以修饰全局变量,经它修饰的全局变量都会成为 线程局部存储的变量,缺点它没办法运行期初始化,它的修饰的变量的初始值必须编译期初始化,列如它无法执行如下代码。但是它的实现比 pthread-specific API 更快,因为通常上 会使用特殊的寄存器保存被该关键字修饰的变量。
static __thread int event_fd = init();
  1. 使用Pthread 库的 pthread-specific data 相关API
Pthread Specific API 介绍

线程局部存储数据(thread-specific data) 可以被看成一个二维表。它的行为 pthread_key (线程局部存储数据的key),它的列可被看作为每个线程的 ID。每一个 线程局部存储的key 都是一个不透明的结构体,它的类型是 pthread_key_t 。一个相同的线程局部存储key可以被该进程所有线程使用,并且每个线程都可以通过这个key去设置相关联的不同的值。比如如下表格,线程T1 的 K1 相关联的值为1。线程T2 的K2相关联的值为4。

keysT1 ThreadT2 Thread
K112
K234

它的相关API 如下

int pthread_once(pthread_once_t *once_control,void (*init_routine)(void));
void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value); 
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*)); 
int pthread_key_delete(pthread_key_t key); 
pthread_key

每一个线程局部存储的key 在被使用前,必须通过 pthread_key_create API进行初始化。这些 key 会自动的在该进程退出的时候释放,也能通过相关API进行释放。每一个进程的 thread-specific key 上限为450个,我们可以通过 PTHREAD_KEYS_MAX 宏来查看上限。

创建 pthread_key

一个线程局部存储key的创建是通过 pthread_key_create创建的,这个创建好的线程局部存储数据key相关联的值会被设置为NULL,并且包括即将创建的线程相关联的值也会一并初始化为NULL。
举个列子,一个Main线程先创建了一个 全局的 pthread_key,然后调用了 pthread_key_create API去创建相关key,这个时候线程局部存储数据为NULL。紧接着它创建了子线程T1,这个时候子线程T1也能使用这个全局的pthread_key,并且对应子线程的线程局部存储数据会被初始化为NULL。
对于每一个 pthread_key_t 对象它只能调用一次 pthread_key_create api(除非调用了pthread_delete后才可以再次创建)。如果我们调用多次那么会有多个线程局部存储数据被创建,此时就会造成资源泄漏。请看如下代码段

/* a global variable */
static pthread_key_t theKey;
/* thread A */
...
pthread_key_create(&theKey, NULL);   /* call 1 */
... 
/* thread B */
...
pthread_key_create(&theKey, NULL);   /* call 2 */
...

在这个列子中Thread1 与 2 并发运行,假设Call 1 发生于 Call2之后。线程A 调用Call1 会创建一个线程局部存储key1 ,key1 会被保存到theKey中。之后Call2 被线程B调用,线程B也会创建一个线程局部存储对象key2,它也被保存到了theKey中,那么之前的key1数据就会被覆盖。
这个时候当线程A 使用 thekey关联的数据时候,它以为是key1对应的数据,实际上却是key2关联的数据。它会造成俩个后果

  1. Thread Specific 资源被泄漏,当我们再次创建的时候可能返回没有足够的 thread specific 资源
  2. 当线程通过 theKey 获取资源的时候,它获取的不是其自己本身的资源只能获取为初始化的NULL值,这样说是线程局部存储线程间相互不影响,但实际上却被线程B 影响了。

它可以通过如下俩个方式解决
1.使用一个单列或者只初始化一次的函数,调用pthread_key_create函数去创建资源为每一个 pthread_key_t对象
2.在Main线程创建其他线程前,调用pthread_key_create 函数去创建该 pthread_key_t 对象。

析构回调

我们可以看到 pthread_create 的第二个参数是一个 void(func)(void*) 的回调函数,该回调函数会在每一个线程退出的时候被调用,这个机制运行我们申请的 thread-specific-data 自动的释放,这个很像RAII机制。
那只要我们设置了这个回调函数,那么当线程退出的时候,它都会带着相关联的 pthread_specific数据被触发调用。这个函数的void
参数就是每一个线程相对应的 pthread_specific数据。
举个列子,下面这个列子就是当我们通过调用pthread_setspecific API 去设置一个堆上的对象的时候,我们可以不用担心它内存泄漏,线程退出的时候自动会调用Destructor 释放它。

typedef struct {
        FILE *stream;
        char *buffer;
} data_t;
...
void destructor(void *data)
{
        fclose(((data_t *)data)->stream);
        free(((data_t *)data)->buffer);
        free(data);
        *data = NULL;
}
pthread_key_t gobal_key;
pthread_key_create(&gobal_key,destructor);
Thread_key 资源的释放

pthread_key的资源可以被释放,通常用于 thread-specific data key资源使用太多的场景下需要释放已经不使用的key资源,那么我们可以调用这个API。但是调用这个pthread_key_delete API并不会调用相关联的 destructor 回调函数去释放每个线程的 pthread-specific-data,因为可能调用 pthread_delete 的线程不使用该key 的线程局部存储数据,但是其他线程还会使用相关联的数据,所以pthread_delete 的实现并不会调用 析构回调去释放 线程局部存储数据。
另外使用 pthread_key_delete 过后的key是未定义行为,并且调用pthread_delete之后当线程退出时与该key相关联的回调不会再被调用。那么调用这个 pthread_key 的时机就很重要了,只有在所有线程都已经释放了 Thread_Specific 资源后才能调用它。

int pthread_key_delete(pthread_key_t key); 
使用 thread-specific data

我们通过 pthread_getspecific 和 pthread_setspecific 函数来使用线程局部存储数据。

void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value); 

因为它的参数是void*,所以支持它是任何类型的数据。注意当我们调用 pthread_setspecific 设置值的时候,它会覆盖老的值,那么就可能出现资源泄漏,并且这个资源永远也无法找回。如下下面这个代码就发生了内存泄漏。

private_data = malloc(...);
pthread_setspecific(key, private_data);
private_data_b = malloc(...);
pthread_setspecific(key, private_data_b);

在设置一个新资源的时候去先释放老资源是我们每一个使用者的责任。列如我们可以下面这样操作

int swap_specific(pthread_key_t key, void **old_pt, void *new)
{
        *old_pt = pthread_getspecific(key);
        return pthread_setspecific(key, new);
}
析构回调和设置资源可能引发的bug

当我们使用线程局部存储资源的时候,如果我们设置了destructor 回调函数,当我们释放 线程局部存储资源的时候必须确保我们是否完毕后,把线程局部存储资源设为NULL,否则可能出现bug。

pthread_key_create(&key, free);
...
...
private_data = malloc(...);
pthread_setspecific(key, private_data);
...
/* bad example! */
...
pthread_getspecific(key, &data);
free(data);

这里线程退出会调free函数,那么free函数会二次释放 private_data会造成未定义行为。当线程退出的时候,如果相应的 thread-specific data 不为NULL,那么相关联的 destructor 回调就会被调用,为确保安全我们应该可以使用

/* better example! */
...
pthread_getspecific(key, &data);
free(data);
pthread_setspecific(key, NULL);
...

这样使用的话,当线程退出,它发现相对应的 thread-specific 数据为NULL,那么 destructor 回调就不会被调用。

boost::thread_specific_ptr

boost 也有 thread_specific_ptr 的实现,它使用起来很像智能指针。reset 的时候就会调用析构回调释放资源,其实它就是对上面所讲的一个thread_specific API的一个封装。 使用这个 类必须是全局类型的定义,比如全局定义 或者 被static 修饰的定义。

template <typename T>
class thread_specific_ptr
{
public:
    thread_specific_ptr();
    explicit thread_specific_ptr(void (*cleanup_function)(T*));
    ~thread_specific_ptr();

    T* get() const;
    T* operator->() const;
    T& operator*() const;

    T* release();
    void reset(T* new_value=0);
};
pthread_once

它有只执行一次 init_routinue 函数的功能,可用于pthread_key_t 对象的初始化 。

#include <pthread.h>
int  pthread_once(pthread_once_t  *once_control,  void  (*init_routine) (void));

1.该函数可以保证在多线程的环境下 , 可以保证 init_routine 函数指针指向的函数只被调用一次。
2.once_control参数需要通过 PTHREAD_ONCE_INIT 宏来初始化,否则行为未定义。当其他线程调用pthread_once以相同的参数执行的时候,这个时候init_routine并不会被执行,线程会从pthread_once 返回 对 once_control 参数的影响就相当于没有调用。once_control 使用的时候一般设置为全局变量并且必须使用PTHREAD_ONCE_INIT宏来初始化。
3.如果在执行pthread_once的时候,线程被pthread_cancel 取消的话,认为 init_routine并没有初始化,由其他线程初始化。
4.对于该函数,它被应用于只初始化一次的场景。
5.每一个pthread_once_t 对象调用pthread_once函数只会生效一次。(无论全局还是局部变量)

单列应用
#include<pthread.h>
#include<iostream>
#include<stdlib.h>
using namespace std;

template<class T>
class Singleton
{
 public:
 static  T& instance()
 {
    pthread_once(&once,Init);
    return *value_;
 }
  private:
 Singleton();
 ~Singleton();
 
  static void Destory()
  {
    typedef  char   COMPELETE_TYPE[(sizeof(T) == 0)? -1 : 1]
    COMPELETE_TYPE dommy;//不是完整类型 sizeof(T) 为-1 ,那么这里定义的时候 [-1]会触发报错
    (void) dommy; //查看是否是完整类型,如果不是完整类型,不能实例化它
    cout<<"Destory" << endl;
    delete value_;
  }
 static void Init(void)
 {
    value_ = new T();
    cout << "Init "<<endl;
    atexit(Destory);
 }
 static T * value_;
 static pthread_once_t once;
};
template<class T>
 T * Singleton<T>::value_ = NULL;
template<classT>
Singleton<T>:: once == PTHREAD_ONCE_INIT;
int main()
{
   int &  object = Singleton<int>::instance();
   int &  object2 = Singleton<int>::instance();
   cout << object<<endl;
   return 0;
}
初始化pthread_key_t

使用pthread_once 去初始化 TSO数据,防止多个线程同时调用pthread_key_create 造成的多个key被创建出来的资源泄漏。

template<class T>
class SpecificPtr {
 public:
  SpecificPtr() {
    pthread_once(&once,InitKey);
  }

  T* GetInstance() {
     T* Specific = pthread_getspecific(key);
     if(T == NULL) {
       Specific = new T();//使用pthread 库 new/malloc 都是线程安全的
       pthread_setspecfic(key,Specific);
     }
     return Specific;
  }
 private:
   void InitKey() {
     pthread_key_create(&key_, NULL); //这里可能造成内存泄漏,如果线程退出前没有调用GetAPI去释放相应资源
   }
 private:
   static pthread_once_t once_;
   static pthread_key_t*  key_;
};
template<class T>
SpecificPtr<T>::once_ = PTHREAD_ONCE_INIT;
template<class T>
SpecificPtr<T>::key_ = NULL;
参考

https://www.ibm.com/support/knowledgecenter/en/ssw_aix_71/com.ibm.aix.genprogc/thread_specific_data.htm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值