Oceanbase代码有关语言和编译器的奇技淫巧(一)
–TSI Factory懒人利器
在oceanbase各个server的读写请求处理逻辑中,经常会遇到需要某个临时对象来保存中间参数或中间结果,比较典型的是线程在处理请求时需要将收到的buffer反序列化到类似ObScanParam这样的对象中,然后再将对象传给到具体的处理方法,执行得到的结果也是要通过类似ObScanner这样的对象传出,序列话到buffer中,再传递给网络线程回包。因此这样对象的作用域仅仅限于网络请求的处理函数中,调用伪代码示意如下:
- void handlePacket(ObPacket &pkt)
- {
- ObScanParam scan_param;
- ObScanner scanner;
- scan_param.deserialize(pkt.get_req_buffer());
- handle_scan(scan_param, scanner);
- scanner.serialize(pkt.get_res_buffer());
- response(pkt);
- }
其中scan_param和scanner做为handlePacket函数的栈对象是比较合理,而ob最初版本的代码也确实是这样写的,但是当我们做性能测试的时候发现这里会有问题,这两个对象要对buffer执行序列化和反序列化,因此他们本质上是内存容器,这就意味着在他们的生命周期中要使用额外的内存,因此它们要么在使用的时候临时申请和释放内存,要么就把对象本身做大尽量使用栈上空间,避免每次都从堆上申请内存。但是无论怎么做都因为这种在栈上使用临时对象的做法,都有比较大的构造和析构成本。有没有简单的办法重用这样的对象呢,TSIFactory因此而诞生,先来看看它的使用方法:
- ObScanner *scanner = GET_TSI(ObScanner);
不同线程通过GET_TSI调用得到的是不同的实例,而在一个线程内部每次调用得到的都是同一个对象,在线程退出的时候自动析构,因此上述代码通过使用TSIFactory简单改造,可以实现对象的重用:
- void handlePacket(ObPacket &pkt)
- {
- ObScanParam *scan_param = GET_TSI(ObScanParam);
- scan_param->reset();
- ObScanner *scanner = GET_TSI(ObScanner);
- scanner->reset();
- scan_param->deserialize(pkt.get_req_buffer());
- handle_scan(*scan_param, *scanner);
- scanner->serialize(pkt.get_res_buffer());
- response(pkt);
- }
下面来看看GET_TSI的实现,宏GET_TSI封装了对一个模板函数的调用:
- template <class T>
- T *get_instance()
- {
- static __thread T *instance = NULL;
- if (NULL == instance && INVALID_THREAD_KEY != key_)
- {
- ThreadSpecInfo *tsi = (ThreadSpecInfo*)pthread_getspecific(key_);
- if (NULL == tsi)
- {
- tsi = new(std::nothrow) ThreadSpecInfo();
- pthread_setspecific(key_, tsi);
- }
- instance = tsi->get_instance<T>();
- }
- return instance;
- }
代码比较简单,这里使用了gcc的特性,“static __thread”。有关它的特性,直接摘一段别人的中文说明如下(摘自http://blog.csdn.net/liuxuejiang158blog/article/details/14100897):
__thread是GCC内置的线程局部存储设施,存取效率可以和全局变量相比。__thread变量每一个线程有一份独立实体,各个线程的值互不干扰。 __thread使用规则:只能修饰POD类型(类似整型指针的标量,不带自定义的构造、拷贝、赋值、析构的类型,二进制内容可以任意复制memset,memcpy,且内容可以复原),不能修饰class类型,因为无法自动调用构造函数和析构函数,可以用于修饰全局变量,函数内的静态变量,不能修饰函数的局部变量或者class的普通成员变量,且__thread变量值只能初始化为编译器常量(值在编译器就可以确定const int i=5,运行期常量是运行初始化后不再改变const int i=rand())。
通过上面的说明,可以理解为什么只能用__thread声明指针而不是对象本身了,那么随之而来的就是另一个问题,什么时候释放这个对象。这里就靠代码中的ThreadSpecInfo来解决对象释放的问题,通过注册pthread specific的回调函数来在线程退出时析构这些tsi对象,而这里想要想清楚,如何管理多个不同类型对象的释放问题,如果保存不同类型的指针?如何调用他们的析构函数?还是来看一段代码:
- class TSINodeBase
- {
- public:
- TSINodeBase() : next(NULL)
- {
- };
- virtual ~TSINodeBase()
- {
- next = NULL;
- };
- TSINodeBase *next;
- };
- template <class T>
- class TSINode : public TSINodeBase
- {
- public:
- explicit TSINode(T *instance) : instance_(instance)
- {
- };
- virtual ~TSINode()
- {
- if (NULL != instance_)
- {
- instance_->~T();
- instance_ = NULL;
- }
- };
- private:
- T *instance_;
- };
看了代码应该基本清楚了,既然需要保存不同类型的指针,那么就用一个基类包来成链表,不同对象的指针分别用不同的TSINode来保存,析构的时候只需要调用每个TSINodeBase的析构函数就好了。
到此为止TSIFactory的实现原理已经清楚了,最后还有一点值得提一下,目前位置实现的TSIFactory管理的对象不能有构造函数参数,这个能不能实现呢,这里的一个难点是既然要支持构造函数参数,那么参数的数量就是不确定的,如何做呢?还是先来看代码:
- #define GET_TSI_ARGS(type, args…) \
- ({ \
- type *__type_ret__ = NULL; \
- Wrapper<type> *__type_wrapper__ = GET_TSI(Wrapper<type>); \
- if (NULL != __type_wrapper__) \
- { \
- __type_ret__ = __type_wrapper__->get_instance(); \
- if (NULL == __type_ret__) \
- { \
- __type_wrapper__->get_instance() = new(std::nothrow) type(args); \
- __type_ret__ = __type_wrapper__->get_instance(); \
- } \
- } \
- __type_ret__; \
- })
如果要求T类不做任何修改,要实现变长参数的无障碍传递,用宏来处理最简单(当然我也没相出来其他办法)。然后就是使用无构造函数参数的Wrapper来包装T类的指针,在第一次使用的时候new出T的对象。
最后解释一下,TSI是什么意思,作者当初写这段代码的时候正在看车,研究大众很给力的TSI发动机,正好也可以解释成thread static instance,就取了这个名字…
这是一个奇技淫巧的系列,后续内容预告一下:
to_cstring的Sfinae魔术
大小为0对象的妙用
弱类型的应用
宏的变长模板
…