我觉得很奇怪。请帮我解释一下。我有一个类,它在一个单独的线程中启动无限循环,还有两个继承它的类。其中一个类实现了将作为std::shared_ptr在外部触发的接口,另一个类将此接口保存为std::weak_ptr。请看下面的代码。对不起,我的代码太多了,我只是想尽量简短地重现错误。为什么有时我会在Sender::notify函数中进行纯粹的虚拟调用?据我所知,std::shared_ptr是可重入的。
#include <iostream>
#include <memory>
#include <thread>
#include <atomic>
#include <list>
#include <mutex>
class Thread : private std::thread {
std::atomic_bool run {true};
public:
Thread() : std::thread([this](){ thread_fun(); }) {}
void thread_fun() {
while (run) loop_iteration();
}
virtual void loop_iteration() = 0;
virtual ~Thread() {
run.exchange(false);
join();
std::cout << "Thread released." << std::endl;
}
};
class Sender : public Thread {
public:
class Signal{
public:
virtual void send() = 0;
virtual ~Signal(){}
};
void add_receiver(std::weak_ptr<Signal> receiver) {
std::lock_guard<std::mutex> lock(receivers_mutex);
receivers.push_back(receiver);
}
void notify() {
std::lock_guard<std::mutex> lock(receivers_mutex);
for (auto r : receivers)
if (auto shp = r.lock())
shp->send(); //Somethimes I get the pure virtual call here
}
private:
std::mutex receivers_mutex;
std::list<std::weak_ptr<Signal>> receivers;
void loop_iteration() override {
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
notify();
}
};
class Receiver : public Thread, public Sender::Signal {
std::atomic_bool notified {false};
public:
void send() override {
notified.exchange(true);
}
private:
void loop_iteration() override {
std::this_thread::sleep_for(std::chrono::milliseconds(250));
std::cout << "This thread was " << (notified? " " : "not ") << "notified" << std::endl;
}
};
int main() {
std::shared_ptr<Thread>
receiver = std::make_shared<Receiver>(),
notifier = std::make_shared<Sender>();
std::dynamic_pointer_cast<Sender>(notifier)->add_receiver(
std::dynamic_pointer_cast<Sender::Signal>(receiver));
receiver.reset();
notifier.reset();
return 0;
}发布于 2021-10-07 16:44:13
多态并不像您在构建和破坏过程中所期望的那样工作。当前类型是仍然存在的派生类型最多的类型。当您在Thread::~Thread中时,对象的Sender部分已经完全销毁,因此调用其重写是不安全的。
当thread_fun试图在构造函数完成之前或在析构函数启动后运行loop_iterator()时,它不会进行多形性分派,而是调用Thread::loop_iteration,这是一个纯虚拟函数(= 0)。
请参阅https://en.cppreference.com/w/cpp/language/virtual#During_construction_and_destruction
下面是一个演示:https://godbolt.org/z/4vsPGYq97
derived对象在1秒后被销毁,此时您将看到输出的变化,这表明被调用的虚拟函数在那个点上发生了变化。
我不确定这段代码是否有效,或者在执行对象的一个成员函数时销毁对象的derived部分是否是未定义的行为。
发布于 2021-10-07 17:06:56
您有一个问题,即假设生成的线程没有立即启动,并且当前线程在执行任何操作之前都有时间初始化当前对象的状态。
这是不成立的,这导致两个问题。
访问当前对象中尚未被initialized.
你在你的破坏者中做了一个小小的假设:
从没有虚拟destructor.
run上获得一个锁,并确保其状态为true,并且所有析构函数都必须将run设置为false。。
你的问题在于:
class Thread : private std::thread {
std::atomic_bool run {true};
public:
Thread()
// Here you are starting a separate thread of execution
// That calls the method thread_fun on the current object.
//
// No problem so far. BUT you should note that "this" object
// is not fully constructed at this point and there is no
// guarantees that the thread you just started will wait
// for this thread to finish before doing anything.
: std::thread([this](){ thread_fun(); })
{}
void thread_fun() {
// The new thread has just started to run.
// And is now accessing the variable `run`.
//
// But `run` is a member and initialized after
// the base class so you have no idea if the parent
// thread has correctly initialized this variable yet.
//
// There is no guratnee that the parent will get to
// the initialization point of `run` before this new thread
// gets to this point where it is using it.
while (run) {
// Here you are calling a virtual function.
// The trouble is that virtual functions are not
// guranteed to work as you would expect until all the
// constructors of the object have run.
// i.e. from base all the way to most derived.
//
// So you not only have to wait for this base class to
// be full constructed you must wait until the object
// is full constructed before you call this method.
loop_iteration();
}
}
virtual void loop_iteration() = 0;
virtual ~Thread() {
// You have a problem in that std::thread destructor
// is not virtual so you will not always call its destructor
// correctly.
//
// But lets assume it was called correctly.
// When you get to this point you have destroyed the
// the state of all derived parts of your object.
// So the function your thread is running better
// not touch any of that state as it is not all invalid
// and doing so is UB.
//
// If your object had no state then you are fine.
run.exchange(false);
join();
std::cout << "Thread released." << std::endl;
}
};我认为更好的解决方案是使std::线程成为对象的成员,并强制所有线程保持到状态被正确初始化为止(在创建对象的位置)。
class Thread {
std::atomic_bool run;
std::thread thread;
public:
Thread(std::function<void>& hold)
// Make sure run is initialized before the thread.
: run{false}
, thread([this, &hold](){ thread_fun(hold); })
{}
void thread_fun(std::function<void>& hold) {
// Pass in a hold function.
// The creator of your objects defines this
// It is supposed to make objects created until you
// have all the state correctly set up.
// once it is you allow any threads that have called
// hold to be released so they can execute.
hold();
while (run) loop_iteration();
}
virtual void loop_iteration() = 0;
virtual ~Thread() {
run.exchange(false);
join();
std::cout << "Thread released." << std::endl;
}
};然后,您可以创建一个简单的屏障,以便在hold中使用:
class Barrier
{
bool threadsShouldWait = true;
std::conditional_variable cond;
std::mutex mutex;
void wait() {
std::unique_lock<std::mutex> lock(mutex);
cond.wait([&](){return !threadsShouldWait;}, lock);
}
void release() {
std::unique_lock<std::mutex> lock(mutex);
threadsShouldWait = false;
cond.notify_all();
}
}
int main()
{
// Note you don't need to use the same barrier for
// both these variables. I am just illustrating one use case.
Barrier barrier;
std::shared_ptr<Thread> receiver = std::make_shared<Receiver>([&barrier](){barrier.wait();});
std::shared_ptr<Thread> notifier = std::make_shared<Sender>([&barrier](){barrier.wait();});
barrier.release();发布于 2021-10-07 17:07:08
除了Fran ois Andrieux注意到的内容外,您真正的问题是使用this对象在其构建完成之前启动线程运行。它可能看到也可能看不到派生类型的构造,这取决于时间。
它不是像他暗示的那样从构造函数中调用thread_fun。这是在不同的线程上,在未来某个未知的时刻。在这个基类构造函数返回之前,或者在派生类的构造过程中的任何其他随机点,或者更晚的时候,它可能发生在另一个核心上。
在对象准备好使用之前,您无法安全地启动线程的函数。
把造物和让它分开。这是最简单的事。
同时
`virtual ~Signal(){}`不要定义空析构函数。写=default代替。但是,在派生类中使用override,而不在那里使用virtual。
https://stackoverflow.com/questions/69484780
复制相似问题