废话不多说,这个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挺有意思,特此记录下