笔试题
class Runnable {
protected:
Runnable () : thread(NULL) {
pthread_create(&thread, NULL, Runnable::Run, this);
}
~Runnable () {
if (NULL != thread) {
// Destroy thread.
pthread_join(thread, NULL);
}
}
protected:
// Override this interface which extended runnable.
virtual void Run (void) {
}
private:
static void* Run (void* args) {
Runnable* runnable = static_cast<Runnable*>(args);
runnable->Run();
return NULL;
}
private:
pthread_t thread;
};
class Worker : public Runnable {
public:
Worker () : quit(false) {
}
~Worker () {
// Set quit flag.
quit = true;
}
protected:
// Override thread run function.
void Run (void) override {
while (!quit) {
// TODO somethine
}
}
private:
volatile bool quit; // Worker thread quit flag.
// OTHER member variables
};
请找出上面程序的Bug。
案例分析
上面笔试题的代码在一定概率下会崩溃或无响应,原因这与C++成员变量和构造函数的初始化顺序有关。在C++中任何变量或者对象的创建都可以看做如下两个步骤:
- 申请与类型一样大小的内存,基本等同于malloc(sizeof(T));
- 调用该类型的构造函数实现对象的初始化操作,因为申请的内存的值是随机值;
这本身是两个步骤的事情让C++编译器合二为一,程序员在大意的情况下下很可能简化为一步,这就会让BUG有可乘之机。如果是通过层层的继承后类的构造函数,C++会优先调用父类的构造函数,再调用子类的构造函数,以此类推。
说道这里不知道读者们有没有意识到上面代码的Bug所在?问题就出在下面这块代码:
Worker () : quit(false) {
}
Worker是Runnable的子类,也就是说程序会优先执行Runnable的构造函数:
Runnable () : thread(NULL) {
pthread_create(&thread, NULL, Runnable::Run, this);
}
从上面的代码可以看看出Runnable的构造函数创建了一个线程,这个线程的函数因为Worker类的重写而执行Worker::Run()。线程创建完成后程序开始执行Worker的构造函数,即将quit设置成false。问题就出在这里,线程优先于quit的初始化创建,在一定概率上线程函数在执行了quit还没有完成初始化,结果就是线程函数直接退出了,因为线程函数在判断quit值的时候还是一个随机值。程序认为线程创建了,但是线程函数已经退出了,外在表现就是僵死了。
除了上面说的Bug以外,在Worker析构的时候也存在问题,因为C++析构的顺序是优先执行子类的析构函数,再执行父类的析构函数,以此类推。这造成的问题是Worker对象已经执行析构函数了,但是线程函数可能还在执行,如果此时线程函数操作了Worker对象的成员变量就会有崩溃的风险。
一个看似很简单的代码,却因为一些小细节造成难以发觉的BUG,尤其是在CPU核数较少的工位机。因为在核数较多的服务器上,创建的线程可以立即执行,而核数较少时,创建的线程因为没有时间片大概率会落后于父线程,致使BUG难以发觉。
附加题
类内有多个成员变量,而成员变量的初始化顺序是定义成员变量时决定的,而不是构造函数里赋值的顺序决定的,记得网上有类似的笔试题。
class Type {
public:
Type() :b(2), a(1){
}
private:
int a;
int b;
};
上面代码实际的执行顺序是a赋值为1,b赋值为2。