0%

一个编译宏引发的 bug

昨天使用厂内 bthread 库(来源于 brpc,已开源)中的 TimerThread 工具来做定时重试任务,但是在打印返回的 TimerThread ID 时,始终打印的是 0,而 TimerThread 分明已经启动成功了。定位了将近一天,才找到原因,这里分享一下。

问题现象

问题可以简化为如下代码:

1
2
3
4
5
thread::TimerThread temp_thread;
if (temp_thread.start(NULL)) {
LOG(INFO) << "start failed";
}
LOG(INFO) << "Temp Thread id: " << temp_thread.thread_id();

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
2
3
4
5
6
7
...
const int ret = pthread_create(&_thread, NULL, TimerThread::run_this, this);
if (ret) {
return ret;
}
_started = true;
LOG(INFO) << "In bthread Timer thread id " << _thread;

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
internal::FastPthreadMutex _mutex;    // protect _nearest_run_time

namespace internal {
#ifdef BTHREAD_USE_FAST_PTHREAD_MUTEX
class FastPthreadMutex {
public:
FastPthreadMutex() : _futex(0) {}
~FastPthreadMutex() {}
void lock();
void unlock();
bool try_lock();
private:
DISALLOW_COPY_AND_ASSIGN(FastPthreadMutex);
int lock_contended();
unsigned _futex;
};
#else
typedef base::Mutex FastPthreadMutex;
#endif

由于 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
2
TRACE: 04-20 20:17:04:   * 0 baidu/base/bthread/bthread/timer_thread.cpp:163] In bthread Timer thread id 139925091317504
TRACE: 04-20 20:17:04: * 0 baidu/vnet/dedicatedconn/src/dedicatedconn_cli_main.cpp:1732] Temp Thread id: 139925091317504

进一步验证

这里我写了一个简单的验证程序,该程序包含三个文件:

common.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct A {
int a[10];
};

#ifdef BTHREAD_USE_FAST_PTHREAD_MUTEX
typedef int LOCK;
#else
typedef struct A LOCK;
#endif

class TimerThread {
public:
TimerThread();
int get_magic() {
std::cout << "In TimerThread sizeof(t) is " << sizeof(*this) << std::endl;
return magic_number;
}

public:
LOCK l;
int magic_number;
};

common.cpp

1
2
3
4
5
#include <iostream>
#include "common.h"

TimerThread::TimerThread(): magic_number(12345) {}

use_common.cpp

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include "common.h"

int main(void)
{
TimerThread t;
std::cout << "In main function: direct get magic number " << t.magic_number << std::endl;
std::cout << "In main function: sizeof is " << sizeof(t) << std::endl;
std::cout << "In main function: call get_magic " << t.get_magic() << std::endl;
}

按照如下方式生成可执行文件:

  1. 编译 common.o,定义 BTHREAD_USE_FAST_PTHREAD_MUTEX 编译宏:
1
g++ -g -c common.cpp -DBTHREAD_USE_FAST_PTHREAD_MUTEX
  1. 编译 use_common.o,不定义 BTHREAD_USE_FAST_PTHREAD_MUTEX 宏:
1
g++ -g -c use_common.cpp
  1. 生成可执行程序:
1
g++ -g -o use_common common.o use_common.o

执行该程序可以看到,无论是直接访问 magic_number,还是调用内联成员函数,都没有得到正确的值。且即使在内联函数内部,得到的 TimerThread 大小也和应用程序看到的一致:

1
2
3
4
5
./use_common
In main function: direct get magic number 4196288
In main function: sizeof is 44
In TimerThread sizeof(t) is 44
In main function: call get_magic 4196288

接下来将 get_magic 定义为非内联函数,即:将 get_magic 的定义放到 common.cpp 中,其他代码保持不变,编译方法也保持不变,再次运行该程序,运行结果如下:

1
2
3
4
5
$ ./use_common
In main function: direct get magic number 4196288
In main function: sizeof is 44
In TimerThread sizeof(t) is 8
In main function: call get_magic 12345
  • 在应用程序内直接访问 magic_number 成员变量得到错误值,这是因为应用程序编译时看到的 TimerThread 类型定义和该对象的实际内存分布并不一致
  • 在应用程序内调用 get_magic 成员函数返回的 magic_number 为正确值,这是因为 get_magic 函数是在编译 common.o 时编译的get_magic 函数的确能够按照该对象构造时的定义解释这个对象,所以能够得到正确结果
  • 在应用程序内看到的 TimerThread 大小为 44,而在 TimerThread 成员函数内部看到的对象大小为 8。sizeof 的结果在编译器就已经决定了,符合预期

总结

当同一个类对象的成员变量在不同的编译单元中显示为不同的值时,应该重点排查这些编译单元看到的类型定义是完全一致,造成不一致的可能原因有:

  • 包含的头文件版本不同
  • 类型的定义受编译宏控制,而不同编译单元可能使用不同的编译宏