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

wal*_*lly 7 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();
}
Run Code Online (Sandbox Code Playgroud)

我希望该程序能打印出来:

I am C.
I am C.
Run Code Online (Sandbox Code Playgroud)

相反它打印:

I am C.
I am A.
Run Code Online (Sandbox Code Playgroud)

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

如何确保虚拟通话?

Sam*_*hik 9

显示的代码表现出竞争条件和未定义的行为.

在你的main()中:

C c;

// ...

a->set_ready();
Run Code Online (Sandbox Code Playgroud)

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

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

join()是超类的析构函数.没有什么可以阻止C被摧毁.当超类被破坏时,析构函数只会暂停并等待加入线程,但C会立即开始销毁!

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

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

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

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

这就是竞争条件.

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

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