线程内的虚拟调用忽略派生类

时间:2016-12-09 00:37:37

标签: c++ multithreading thread-safety

在以下程序中,我从一个线程中进行虚拟调用:

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>

class A {
public:
    virtual ~A() { t.join(); }
    virtual void getname() { std::cout << "I am A.\n"; }
    void printname() 
    { 
        std::unique_lock<std::mutex> lock{mtx};
        cv.wait(lock, [this]() {return ready_to_print; });
        getname(); 
    };
    void set_ready() { std::lock_guard<std::mutex> lock{mtx}; ready_to_print = true; cv.notify_one(); }
    void go() { t = std::thread{&A::printname,this}; };

    bool ready_to_print{false};
    std::condition_variable cv;
    std::mutex mtx;
    std::thread t{&A::printname,this};
};

class B : public A {
public:
    int x{4};
};

class C : public B {
    void getname() override { std::cout << "I am C.\n"; }
};

int main()
{
    C c;
    A* a{&c};
    a->getname();
    a->set_ready();
}

我希望该程序可以打印出来:

I am C.
I am C.

但是instead it prints

I am C.
I am A.

在程序中,我等到派生对象完全构造后再调用虚拟成员函数。但是,线程在对象完全构造之前启动。

如何确保虚拟通话?

3 个答案:

答案 0 :(得分:9)

显示的代码显示竞争条件和未定义的行为。

在你的主要():

C c;

// ...

a->set_ready();

set_ready()返回后,执行线程立即离开main()。这导致c 立即销毁 ,从超类C开始,继续销毁B,然后{{1 }}

A在自动范围内声明。这意味着只要c返回,它就会消失。加入合唱团看不见。它不复存在了。它不复存在。这是一个前对象。

您的main()位于超类的析构函数 中。什么都不会阻止join()被摧毁。当超类被破坏时,析构函数只会暂停并等待加入线程,但C将立即开始销毁!

一旦C超类被销毁,其虚拟方法就不再存在,并且调用虚函数将最终在基类中执行虚函数。

同时另一个执行线程正在等待互斥锁和条件变量。竞争条件是你不能保证其他执行线程会在父线程销毁C之前被唤醒并开始执行,它会在发出条件变量信号后立即执行。

表示条件变量的所有信号都表明,无论执行线程在条件变量上旋转,执行线程都将开始执行。最终。该线程可以在一个非常负载的服务器上,在通过条件变量发出信号后,在几秒钟后开始执行。它的目标很久以前就消失了。它处于自动范围,C将其销毁(或者,main()子类已经被销毁,C的析构函数正在等待加入线程。)

您正在观察的行为是在A在接收到条件变量的信号并解锁之后C在进行虚拟方法调用之前销毁std::thread超类的父线程互斥。

这就是竞争条件。

此外,在销毁虚拟对象的同时执行虚拟方法调用已经不是首发。这是未定义的行为。即使执行线程在重写方法中结束,其对象也会被另一个线程同时销毁。无论你转向哪个方向,你都非常紧张。

经验教训:绑定std::thread以执行this对象中的某些内容是未定义行为的雷区。有办法正确地做到这一点,但这很难。

答案 1 :(得分:2)

这是最有可能的事件序列:

  • 构造对象的A部分,它启动一个线程
  • 构建对象的B部分。
  • 构造对象的C部分。
  • 在主线程上调用
  • getname,打印出“我是C!”因为它是C。
  • 主线程通知另一个线程(我称之为打印线程)
  • main开始返回。
  • 对象的C部分被破坏。
  • 物体的B部分被破坏。
  • 对象的A部分被破坏......但这会阻塞,直到打印线程退出。
  • 现在主线程被阻止,操作系统切换到打印线程。
  • 打印线程调用getname,打印出“我是A!”因为它是一个A(对象的C和B部分现在已被破坏)。
  • 打印线程退出
  • 主线程唤醒,完成销毁A部分并退出程序。

为了可靠地获得预期的行为,您需要等待打印线程退出,然后 }的结束main

答案 2 :(得分:0)

其他答案是明确的,但并未显示可能的解决方法。这是带有其他变量并等待的相同程序:

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>

class A {
public:
    virtual ~A() { t.join(); }
    virtual void getname() { std::cout << "I am A.\n"; }
    void printname()
    {
        std::unique_lock<std::mutex> lock{mtx};
        cv.wait(lock, [this]() {return ready_to_print; });
        getname();
        printing_done = true;
        cv.notify_one();
    };
    void set_ready() { std::lock_guard<std::mutex> lock{mtx}; ready_to_print = true; cv.notify_one(); }
    void go() { t = std::thread{&A::printname,this}; };

    bool ready_to_print{false};
    bool printing_done{false};
    std::condition_variable cv;
    std::mutex mtx;
    std::thread t{&A::printname,this};
};

class B : public A {
public:
    int x{4};
};

class C : public B {
public:
    ~C() 
    {
        std::unique_lock<std::mutex> lock{mtx};
        cv.wait(lock, [this]() {return printing_done; });
    }
    void getname() override { std::cout << "I am C.\n"; }
};

int main()
{
    C c;
    A* a{&c};
    a->getname();
    a->set_ready();
}

Prints

I am C.
I am C.