线程中的虚拟呼叫忽略派生类

Virtual call within thread ignores derived class

本文关键字:派生 呼叫 虚拟 线程      更新时间:2023-10-16

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

#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.

但它打印:

I am C.
I am A.

在程序中,我等到调用虚拟成员函数之前完全构造了派生对象。但是,该线程是在对象完全构造之前开始的。

如何确保虚拟调用?

所示代码表现出种族条件和不确定的行为。

在您的主()中:

C c;
// ...
a->set_ready();

立即,set_ready()返回后,执行线程离开main()。这导致 c的立即破坏,从超级类C开始,然后继续销毁B,然后是A

c在自动范围中声明。这意味着一旦main()返回,它就消失了。加入了合唱团隐形。不再了。它不再存在。这是一个前对象。

您的join()在超级阶级的Destructor 中。没有什么可以阻止C被摧毁。当 superclass 被销毁时,驱动器只会停下来等待加入线程,但是C将立即开始销毁!

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

同时,另一个执行线程在静音和条件变量上等待。种族条件是您不能保证其他执行线程将醒来并开始在父螺纹破坏C之前,它在发出条件变量后立即进行。

所有发出条件变量的信号都为您提供的是,任何执行线程都在条件变量上旋转,执行线程将开始执行。最终。该线程可以在非常加载的服务器上开始执行几秒钟后,之后通过条件变量发出信号。它的对象很久以前就消失了。它是在自动范围中的,main()将其销毁(或者,C子类已经被销毁,A的distructor正在等待加入线程)。

您要观察到的行为是在std::thread从条件变量接收信号并解锁其Mutex之后,在CC_18开始销毁C超级阶级的父螺纹。

那是种族条件。

此外,执行虚拟方法调用同时销毁虚拟对象已经不是启动器。这是不确定的行为。即使执行线程最终以覆盖方法最终形成,其对象也会同时被另一个线程销毁。到那时,您几乎都被搞砸了。

课程学习:在this对象中执行某件事是不确定行为的雷区。有正确的操作方法,但很难。

这是最可能的事件序列:

  • 对象的一部分是构造的,它启动了线程
  • 构建对象的B部分。
  • 构建对象的C部分。
  • getname在主线程上被称为"我是C!"。因为它是一个c。
  • 主线程通知另一个线程(我将其称为打印线程)
  • main开始返回。
  • 对象的C部分被破坏。
  • 对象的B部分被破坏。
  • 对象的一部分被破坏了...但是这会阻止打印线出口。
  • 现在将主线程阻止了OS开关到打印线程。
  • 打印线程调用getname,它打印"我是!"因为它是一个A(现在已销毁了物体的C和B部分)。
  • 打印线出口
  • 主线程醒来,完成销毁零件并退出程序。

要可靠地获得预期的行为,您需要等待打印线程在 main}之前退出

其他答案是确定的,但没有显示可能的修复。这是具有其他变量和等待的同一程序:

#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();
}

打印:

I am C.
I am C.