在下面的程序中,我有一个线程内的虚拟调用:
#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.
在程序中,我等到派生对象完全构造完成后才调用虚拟成员函数。然而,线程在对象完全构造之前启动。
虚拟通话如何放心?
最佳答案
显示的代码表现出竞争条件和未定义的行为。
在你的 main() 中:
C c;
// ...
a->set_ready();
set_ready()
返回后,执行线程立即离开 main()
。这导致 c
的立即销毁,从父类(super class) C
开始,并继续销毁 B
,然后是 A
。
c
在自动范围内声明。这意味着一旦 main()
返回,它就消失了。加入了无形的合唱团。它已经不复存在了。它不复存在了。这是一个前物体。
您的join()
在父类(super class)的析构函数中。没有什么可以阻止 C
被销毁。当 superclass 被销毁时,析构函数只会暂停并等待加入线程,但 C
将立即开始被销毁!
一旦C
父类(super class)被销毁,它的虚方法就不存在了,调用虚函数最终会执行基类中的虚函数。
与此同时,另一个执行线程正在等待互斥锁和条件变量。竞争条件是您无法保证其他执行线程会在父线程销毁 C
之前唤醒并开始执行,父线程会在发出条件变量信号后立即销毁。
发出条件变量信号的所有信息是,无论执行线程在条件变量上旋转,该执行线程都会开始执行。最终。在负载非常大的服务器上,该线程可以在通过条件变量发出信号后几秒钟后开始执行。它的对象很久以前就不见了。它在自动范围内,并且 main()
销毁了它(或者更确切地说,C
子类已经被销毁,并且 A
的析构函数正在等待加入线程)。
您观察到的行为是父线程设法销毁 C
父类(super class),然后 std::thread
在收到来自条件变量的信号,并解锁其互斥量。
这就是竞争条件。
此外,在销毁虚拟对象的同时执行虚拟方法调用已经是行不通的。这是未定义的行为。即使执行线程在重写的方法中结束,它的对象也会同时被另一个线程销毁。到那时,无论你转向哪个方向,你都完蛋了。
经验教训:装配 std::thread
以在 this
对象中执行某些操作是未定义行为的雷区。有很多方法可以正确地做到这一点,但这很难。
关于c++ - 线程内的虚拟调用忽略派生类,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41051306/