一个C++多态多线程环境下的崩溃BUG分析

废话不多说,这个BUG也是自己写代码设计线程类,偶然之间触发的代码

#include <pthread.h>

#include <stdio.h>



class thread_base

{

    pthread_t thread = -1;

    static void* thread_entry(void* thiz)

    {

        ((thread_base*)thiz)->run();

        return NULL;

    }

public:

    void start()

    {

        pthread_create(&thread,NULL,thread_entry,this);

    }

    thread_base() {}

    virtual void run() = 0;

    virtual ~thread_base()

    {

        if (thread != -1)

            pthread_join(thread,NULL);

    }

};



class mythread : public thread_base

{

    virtual void run() override

    {

        printf("Test
");

    }

};



int main()

{

    mythread thread;

    thread.start();

}

这个代码其实也挺简单,就是一个线程对象先启动线程,然后马上在析构函数里面等待线程结束

但是,这样会导致程序终止,我在VSCode跑下来的结果是这样子的:

这里直接提示调用了纯虚函数,起初我也困惑为啥会这样,其实原理也很简单

既然是和虚函数有关,我们修改下代码,尝试打印虚表地址,修改之后的代码是这样,打印的地方看注释:

#include <pthread.h>
#include <stdio.h>

class thread_base
{
    pthread_t thread = -1;
    static void* thread_entry(void* thiz)
    {
        ((thread_base*)thiz)->run();
        return NULL;
    }
public:
    void start()
    {
        pthread_create(&thread,NULL,thread_entry,this);
    }
    thread_base() {}
    virtual void run() = 0;
    virtual ~thread_base()
    {
        //等待时的虚表地址
        printf("virtual table address of this: %p
", *((void**)this));
        if (thread != -1)
            pthread_join(thread,NULL);
    }
};

class mythread : public thread_base
{
    virtual void run() override
    {
        printf("Test
");
    }
};

int main()
{
    mythread thread;
    //执行前的虚表地址
    printf("virtual table address of mythread: %p
", *((void**)&thread));
    thread.start();
}

这个代码的运行结果是这样:

到了这一步已经很明显了,析构的地方虚表地址确实变了,然后执行了纯虚函数

但是为啥会这样,还有很多细节没有确认,这里就要用到一点操作系统的知识来解释,直接分析这个bug,代码的流程可以认为是这样:

1.类初始化,创建线程,但是线程并未马上得到调度,子类的run方法未执行,此时虚表地址正常,就是子类的地址

2.main函数马上结束,类对象析构,子类的析构先执行,然后是父类的析构,但是这里注意,到了父类的析构函数,虚表地址就被替换为父类的虚表地址,这个时候等待线程,让线程得到了调度,但是虚表地址变了,执行到了纯虚函数,程序终止

这里用到了一些操作系统知识和一些编译器的实现细节,其实这句话也印证了《Effective C++》里面的一条建议:不要在构造函数和析构函数里面调用虚函数,只不过我这里不是直接调用,而是因为线程调度间接调用虚函数,因为这个BUG挺有意思,特此记录下