muduo库的ThreadLocal类剖析

本文详细介绍了线程特定数据的概念及其实现原理,探讨了POSIX线程库如何通过四个关键函数操作线程特定数据,并深入分析了muduo库中ThreadLocal类的设计思想与实现细节。


首先来看一个概念:线程特定数据

>>>在单线程程序中,我们经常用全局变量共享数据。多线程环境下,全部变量被所有线程所共有。

>>>但有时应用程序设计中有必要提供线程私有的全局变量,仅在某个线程中有效

>>>POSIX线程库通过维护一定的数据结构来解决这个问题,这些数据称之为线程特定数据(Thread-specific Data,或TSD)

>>>对于POD类型,可以用__thread来解决。


POSIX线程库通过四个函数操作线程特定数据,分别是pthread_key_create,pthread_key_delete,pthread_getspecific,pthread_setspecific

create创建一个key,一旦一个线程创建了一个key,那么所有的线程也都有这个key。我们可以为特定的线程指定特定的数据,可以使用set指定,get获取。那么这些数据就是每个线程所私有的,这样不同的线程的key就指向了不同的数据。delete是删除这个key,不是删除数据,删除数据要在create的时候指定一个回调函数,由回调函数来销毁数据,这个数据是堆上的数据就可以销毁。


ThreadLocal类的视图如下:



分析如下:

#ifndef MUDUO_BASE_THREADLOCAL_H
#define MUDUO_BASE_THREADLOCAL_H

#include <muduo/base/Mutex.h>  // MCHECK

#include <boost/noncopyable.hpp>
#include <pthread.h>

namespace muduo
{

template<typename T>
class ThreadLocal : boost::noncopyable
{
 public:
  ThreadLocal()
  {
    //构造函数中创建key,数据的销毁由destructor来销毁
    MCHECK(pthread_key_create(&pkey_, &ThreadLocal::destructor));  
  }

  ~ThreadLocal()
  {
    //析构函数中销毁key
    MCHECK(pthread_key_delete(pkey_));
  }

  //获取线程特定数据
  T& value()
  {
    T* perThreadValue = static_cast<T*>(pthread_getspecific(pkey_)); //通过key获取线程特定数据
    if (!perThreadValue)  //如果是空的,说明特定数据还没有创建,那么就空构造一个
    {
      T* newObj = new T();
      MCHECK(pthread_setspecific(pkey_, newObj));  //设置特定数据
      perThreadValue = newObj;   //返回
    }
    return *perThreadValue;   //返回对象引用,所以需要*
  }

 private:

  static void destructor(void *x)
  {
    T* obj = static_cast<T*>(x);
    typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];   //检测是否是完全类型
    T_must_be_complete_type dummy; (void) dummy; 
    delete obj;   //如果是,我们就可以删除它了
  }

 private:
  pthread_key_t pkey_;    //key的类型是pthread_key_t类型
};

}
#endif


muduo库在key上面使用的trick:

muduo库ThreadLocal类只有一个成员pkey_,它是一个pthread_key_t类型。但是我们发现,muduo库注册的destroy()函数直接将它的入口参数x指针强制转化为T类型指针。然后直接调用delete。这是什么原因呢?

在pthread_key_create()函数中,传入了一个&ThreadLocal::destructor的成员指针,当线成局部变量销毁时,如果传入的第二个参数不为NULL,系统将调用该函数取销毁实际的数据。由于是类成员函数,隐藏了一个指针是this指针,这时候void *x,x的地址实际上就是this的地址,也就是该对象的地址。

上面我们知道,ThreadLocal类只有一个成员,就是pthread_key_t类型成员pkey_(不是指针类型),我们可以再回头看pthread_key_create()函数,我们发现它传入的第一个参数正是pkey_的地址,也就是pthread_key_t*类型。所以我们在destructor()函数中使用

 T* obj = static_cast<T*>(x);

 delete obj; 

由于pkey_是传入pthread_key_create()的第一个参数,所以它的地址就是实际数据存放的地址。而(void*)this == (void*)&pkey_,所以直接强制转化this指针为T*类型,然后以

T*类型的方式delete,就会真实释放线程局部存储的数据。

这种方法相当于使用一个写死已经分配好的地址,去存储真实数据。&pkey->数据,实际上跟普通的指针功能是一样的。不过对外可以表现为一个pthread_key_t类型,相当于起了一个键值名字,看起来好像使用键值寻找指向的数据一样。


下面有部分百度百科对这个的解释:http://baike.baidu.com/link?url=mEco9WH30o91pW57KVOB6oK8irHl5FIpG2m2gcEPrj2HyB5D4j1djgxcySdLQ-tg



测试用例如下:

#include <muduo/base/ThreadLocal.h>
#include <muduo/base/CurrentThread.h>
#include <muduo/base/Thread.h>

#include <boost/noncopyable.hpp>
#include <stdio.h>

class Test : boost::noncopyable
{
 public:
  Test()
  {
    printf("tid=%d, constructing %p\n", muduo::CurrentThread::tid(), this);
  }

  ~Test()
  {
    printf("tid=%d, destructing %p %s\n", muduo::CurrentThread::tid(), this, name_.c_str());
  }

  const muduo::string& name() const { return name_; }
  void setName(const muduo::string& n) { name_ = n; }

 private:
  muduo::string name_;
};

muduo::ThreadLocal<Test> testObj1;
muduo::ThreadLocal<Test> testObj2;

void print()
{
  printf("tid=%d, obj1 %p name=%s\n",
         muduo::CurrentThread::tid(),
         &testObj1.value(),
         testObj1.value().name().c_str());
  printf("tid=%d, obj2 %p name=%s\n",
         muduo::CurrentThread::tid(),
         &testObj2.value(),
         testObj2.value().name().c_str());
}

void threadFunc()
{
  print();
  testObj1.value().setName("changed 1");
  testObj2.value().setName("changed 42");
  print();
}

int main()
{
  testObj1.value().setName("main one");
  print();
  muduo::Thread t1(threadFunc);
  t1.start();
  t1.join();
  testObj2.value().setName("main two");
  print();

  pthread_exit(0);
}

它的输出是这样的:


我们来分析一下结果:

>>>第一部分:这是主线程中第一次执行的结果,主线程中试图利用obj1的value()方法取该线程特定数据,此时线程特定数据还没有创建,所以内部调用T()创建,然后主线程设置线程特定数据的 name为"main one",同时调动打印,由于obj2没有调用value()方法,所以他的name是空的。

>>>第二部分,子线程中没有调用value()直接打印,所以name都为空。此处我们可以看到调用了构造函数,所以构造出的新的obj1,和obj2都没有名字,它们就是线程局部变量,地址也都不同于之前的全局对象的地址。

>>>第三部分,更改了局部对象的名字。

>>>第四部分,由于muduo库pthread_key_create的实现注册了destroy函数,所以子线程终止时,不仅释放key,同时会释放数据,也就是调用test的析构函数。

>>>第五部分,主线程中先打印两个全局对象名字,它们并没有改变。并且主线程结束时也调用了析构函数。


评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值