线程中的虚拟呼叫忽略派生类
Virtual call within thread ignores derived class
在以下程序中,我在线程中有一个虚拟调用:
#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.
- 为什么使用 "this" 指针调用派生成员函数?
- 具有奇怪重复模板模式的派生类中的成员变量已损坏
- 在派生函数中指定void*参数
- 如何通过派生类函数更改基类中的向量
- 如何委托派生类使用其父构造函数?
- 呼叫运营商<<临时
- 如何使用单独文件中的派生类访问友元函数对象
- 派生类销毁的最佳实践是什么
- 如何使用基类指针引用派生类成员
- 派生类是否可以在抽象工厂设计模式中具有数据成员
- 使用基类指针创建对象时,缺少派生类析构函数
- 如何引用基类的派生类?
- 存储模板类型以强制转换回派生<T>
- 需要从 istream 和 ostream 派生 iostream
- 在 C++ 中用派生类型重写成员函数
- 具有多个类、派生类的C++正向声明
- 有没有一种"cleaner"的方法可以在指向基的指针向量中找到派生类的第一个实例?
- 用常见虚拟函数实现的任意组合来实现派生类的正确方法是什么
- 如果基类包含双指针成员,则派生类的构造函数
- 线程中的虚拟呼叫忽略派生类