C++11 std::thread 和虚函数绑定

Posted

技术标签:

【中文标题】C++11 std::thread 和虚函数绑定【英文标题】:C++11 std::thread and virtual function binding 【发布时间】:2015-10-16 13:40:54 【问题描述】:

我遇到了一个奇怪的 C++ 代码行为,不确定它是编译器错误还是只是我的代码的未定义/未指定行为。代码如下:

#include <unistd.h>
#include <iostream>
#include <thread>

struct Parent 
    std::thread t;

    static void entry(Parent* p) 
        p->init();
        p->fini();
    

    virtual ~Parent()  t.join(); 

    void start()  t = std::threadentry, this; 

    virtual void init()  std::cout << "Parent::init()" << std::endl; 
    virtual void fini()  std::cout << "Parent::fini()" << std::endl; 
;

struct Child : public Parent 
    virtual void init() override  std::cout << "Child::init()" << std::endl; 
    virtual void fini() override  std::cout << "Child::fini()" << std::endl; 
;

int main() 
    Child c;

    c.start();
    sleep(1); // <========== here is it

    return 0;

代码的输出如下,这并不奇怪:

Child::init()
Child::fini()

但是,如果函数调用“sleep(1)”被注释掉,输出将是:

Parent::init()
Parent::~fini()

在 Ubuntu 15.04 上测试,gcc-4.9.2 和 clang-3.6.0 显示相同的行为。编译器选项:

g++/clang++ test.cpp -std=c++11 -pthread

它看起来像一个竞争条件(在线程开始之前 vtable 没有完全构建)。这段代码格式不正确吗?编译器错误?还是应该是这样的?

【问题讨论】:

想一想:线程使用了子对象,但是子对象被销毁线程加入之前(因为加入只发生在子对象销毁之后开始)! 加上 sleep 注释,你在析构函数中“调用”虚方法... 一个快速的解决方法是为 Child 创建一个析构函数并从那里调用t.join();。你必须保护你的线程,但我再次说这是一个快速修复。 @KerrekSB 已经给了你正确的答案。顺便说一句,在您的计算机中您看到孩子因睡眠而起作用的事实具有误导性;其他计算机仍然可以看到调用的Parent 函数或Child 然后Parent 有些人在遇到问题时会想,“我知道,我会使用线程”,然后他们就会遇到两个问题。 【参考方案1】:

@KerrekSB commented:

线程使用子对象,但是子对象在线程加入之前就被销毁了(因为加入只发生在子对象的销毁开始之后)。

Child 对象在main 的末尾被销毁。 Child 析构函数被执行,并有效地调用Parent 析构函数,其中Parent 基(没有这样)和数据成员(线程对象)被销毁。当析构函数在基类链中向上调用时,对象的动态类型会发生变化,与构造过程中的变化顺序相反,因此此时对象的类型为Parent

线程函数中的虚拟调用可以发生在调用Child析构函数之前、重叠或之后,在重叠的情况下,有一个线程访问正在更改的存储(实际上是vtable指针)由另一个线程。所以这是未定义的行为。

【讨论】:

【参考方案2】:

这是常见的设计问题;你试图做的是一个经典的反模式。

Parent不能同时是线程管理器,启动线程并等待线程终止:

virtual ~Parent()  t.join(); 

void start()  t = std::threadentry, this; 

还有一个线程对象:

virtual void init()  std::cout << "Parent::init()" << std::endl; 
virtual void fini()  std::cout << "Parent::fini()" << std::endl; 

这是两个截然不同的概念,具有严格不兼容的规范。

(而且线程对象一般没用。)

【讨论】:

你有什么有用的模式来建议解决这个特殊问题吗?

以上是关于C++11 std::thread 和虚函数绑定的主要内容,如果未能解决你的问题,请参考以下文章

c++11的std::thread能用类的成员函数构造一个线程吗?语法怎样?

使用 std::thread 函数 C++11 将指针作为参数传递

带有 winsock 和 std::thread 的 C++ 多线程服务器

混合 C++11 std::thread 和 C 系统线程(即 pthreads)

cpp►C++11标准线程库<thread>

可连接 std::thread 的析构函数