昨天使用厂内 bthread 库(来源于 brpc,已开源)中的 TimerThread 工具来做定时重试任务,但是在打印返回的 TimerThread ID 时,始终打印的是 0,而 TimerThread 分明已经启动成功了。定位了将近一天,才找到原因,这里分享一下。
问题现象
问题可以简化为如下代码:
1 | thread::TimerThread temp_thread; |
TimerThread 的 start 函数返回成功,但是调用 thread_id() 始终返回 0:
1 | TRACE: 04-20 18:43:22: * 0 baidu/vnet/dedicatedconn/src/dedicatedconn_cli_main.cpp:1732] Temp Thread id: 0 |
TimerThread 的 start 函数内部会调用 pthread_create
创建一个线程,通过在 start 函数内部添加打印,可以看到 _thread 已经被正确赋值了:
TimerThread 的 start 函数实现部分:
1 | ... |
TimeThread 的内部打印,thread_id 是正确的:
1 | TRACE: 04-20 18:43:22: * 0 baidu/base/bthread/bthread/timer_thread.cpp:163] In bthread Timer thread id 140271338514176 |
由于 thread_id 函数就是直接返回的 _thread 成员:
1 | pthread_t thread_id() const { return _thread; } |
那为啥成员函数内部打印的结果和调用 thread_id public 函数返回的结果不一致呢,难道碰到了玄学问题?
定位问题
为了定位该问题,将 TimerThread 类的 private 成员直接修改为 public 成员,然后在我自己的程序和 TimerThread 的 start 函数里分别打印对象地址、成员偏移量、成员大小,得到一个重要线索:TimerThread 类里 _mutex 的大小不一致:
应用程序打印:
1 | TRACE: 04-20 18:43:22: * 0 baidu/vnet/dedicatedconn/src/dedicatedconn_cli_main.cpp:1740] Timer _mutex size 40 |
TimerThread 内部打印:
1 | TRACE: 04-20 18:43:22: * 0 baidu/base/bthread/bthread/timer_thread.cpp:171] Timer _mutex size 4 |
由于 _thread 成员定义在 _mutex 成员后面,这就导致了 _thread 在类对象内部的偏移量也不一致。这就造成了对于同一个类对象,两边看到的 _thread 偏移不同,得到的值也就不同。
那为啥 _mutex 的大小两边不一致呢,原来 _mutex 的类型通过了一个编译宏控制:
1 | internal::FastPthreadMutex _mutex; // protect _nearest_run_time |
由于 bthread 库编译的 Makefile 中通过 -DBTHREAD_USE_FAST_PTHREAD_MUTEX
定义了该编译宏,而自己的应用程序编译时没有定义该宏,这就导致两边看到的 TimerThread 类的定义并不完全一致( _mutex 成员大小不一致)。
不知道大家有没有想过一个问题,即使类型定义不一致,但是在应用程序中我并不是直接访问的 _thread 成员,而是调用 thread_id 成员函数得到 _thread 的值。那在调用 TimerThread 的成员函数时,应该是按照 TimerThread 自己所看到的定义返回 _thread 的值,为啥实际上也没有返回正确结果呢?原因就是 thread_id 函数是在类内定义的,所以它是一个内联函数。这就使得它在函数调用点直接内联展开,这就和直接在应用程序访问 _thread 成员效果是等价的。
1 | pthread_t thread_id() const { return _thread; } |
解决问题
在应用程序侧加上 BTHREAD_USE_FAST_PTHREAD_MUTEX 编译宏定义,thread id 打印成功,问题解决。
1 | TRACE: 04-20 20:17:04: * 0 baidu/base/bthread/bthread/timer_thread.cpp:163] In bthread Timer thread id 139925091317504 |
进一步验证
这里我写了一个简单的验证程序,该程序包含三个文件:
common.h
1 | struct A { |
common.cpp
1 |
|
use_common.cpp
1 | #include <iostream> |
按照如下方式生成可执行文件:
- 编译 common.o,定义 BTHREAD_USE_FAST_PTHREAD_MUTEX 编译宏:
1 | g++ -g -c common.cpp -DBTHREAD_USE_FAST_PTHREAD_MUTEX |
- 编译 use_common.o,不定义 BTHREAD_USE_FAST_PTHREAD_MUTEX 宏:
1 | g++ -g -c use_common.cpp |
- 生成可执行程序:
1 | g++ -g -o use_common common.o use_common.o |
执行该程序可以看到,无论是直接访问 magic_number,还是调用内联成员函数,都没有得到正确的值。且即使在内联函数内部,得到的 TimerThread 大小也和应用程序看到的一致:
1 | ./use_common |
接下来将 get_magic 定义为非内联函数,即:将 get_magic 的定义放到 common.cpp 中,其他代码保持不变,编译方法也保持不变,再次运行该程序,运行结果如下:
1 | $ ./use_common |
- 在应用程序内直接访问 magic_number 成员变量得到错误值,这是因为应用程序编译时看到的 TimerThread 类型定义和该对象的实际内存分布并不一致
- 在应用程序内调用 get_magic 成员函数返回的 magic_number 为正确值,这是因为 get_magic 函数是在编译 common.o 时编译的get_magic 函数的确能够按照该对象构造时的定义解释这个对象,所以能够得到正确结果
- 在应用程序内看到的 TimerThread 大小为 44,而在 TimerThread 成员函数内部看到的对象大小为 8。sizeof 的结果在编译器就已经决定了,符合预期
总结
当同一个类对象的成员变量在不同的编译单元中显示为不同的值时,应该重点排查这些编译单元看到的类型定义是完全一致,造成不一致的可能原因有:
- 包含的头文件版本不同
- 类型的定义受编译宏控制,而不同编译单元可能使用不同的编译宏