在以下程序中,我在一个线程中有一个虚拟调用:
#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()在超类的析构函数中.没有什么可以阻止C被摧毁.析构函数只会暂停并等待加入线程,当超类被破坏时,C将立即开始被销毁!
一旦C超类被销毁,其虚拟方法就不再存在,并且调用虚函数将最终在基类中执行虚函数.
同时另一个执行线程正在等待互斥锁和条件变量.竞争条件是你无法保证其他执行线程将在父线程销毁C之前唤醒并开始执行,它在发出条件变量信号后立即执行.
发出条件变量的所有信号都表明,无论执行线程在条件变量上旋转,执行线程都将开始执行.最终.该线程可以在一个非常负载的服务器上,在通过条件变量发出信号之后,在几秒钟后开始执行.它的目标很久以前就消失了.它在自动范围内,并且main()将其销毁(或者更确切地说,C子类已经被销毁,并且A的析构函数正在等待加入该线程).
您正在观察的行为是在收到来自条件变量的信号并解锁其互斥锁之后,在std :: thread转向进行虚拟方法调用之前管理销毁C超类的父线程.
这就是竞争条件.
此外,在销毁虚拟对象的同时执行虚拟方法调用已经是非启动性的.这是未定义的行为.即使执行线程在重写方法中结束,其对象也会被另一个线程同时销毁.无论你转向哪个方向,你都非常紧张.
经验教训:绑定std :: thread来执行此对象中的某些操作是未定义行为的雷区.有办法正确地做到这一点,但这很难.