使用共享指针从另一个线程进行纯虚拟调用
Pure virtual call from another thread using shared pointer
我觉得很奇怪。请帮我解释一下。我有一个 class 在单独的线程中启动无限循环,还有两个 class 继承它。其中一个 classes 将要在外部触发的接口实现为 std::shared_ptr
,而另一个 class 将此接口保持为 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;
}
多态性在构建和销毁过程中并不像您预期的那样起作用。当前类型是仍然存在的最派生类型。当您处于 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
对象在一秒后被销毁,此时您会看到输出发生变化,表明被调用的虚函数在此时发生了变化。
我不确定此代码是否有效,或者在执行其成员函数之一时破坏对象的 derived
部分是否是未定义的行为。
您遇到了一个问题,因为您假设生成的线程不会立即启动,并且当前线程在执行任何操作之前有时间初始化当前对象的状态。
这不成立导致两个问题。
- 您访问了当前对象中尚未初始化的状态。
- 您使用的多态函数在对象完全构建之前不能保证有效。
您在析构函数中做了一个小假设:
- 您从没有虚拟析构函数的对象继承。
- 在对象开始销毁后,您的线程可能仍会访问状态。如果确实如此(访问被破坏)那么它就是 UB。您的线程需要能够检查当前对象状态是否有效(即所有派生 类 必须锁定
run
并确保其状态为真并且所有析构函数必须设置 run
为假。
你的问题出在这里:
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::thread 成为对象的成员,并强制所有线程保持状态,直到状态正确初始化(在创建对象时)。
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();
除了 François Andrieux 指出的内容之外,您真正的问题是您在构造完成之前使用 this
对象启动线程 运行。它可能会也可能不会看到构造的派生类型,具体取决于时间。
它并没有像他暗示的那样从 构造函数调用thread_fun
。它在不同的线程上调用它,在未来某个未知的时间点。它可能发生在这个基础 class 构造函数返回之前的不同核心上,或者在派生 class 的构造过程中的任何其他随机点,或者更晚。
在对象准备好使用之前,您无法安全地启动线程的功能。
将创作与制作分开go。这是最简单的事情了。
同时
virtual ~Signal(){}
不要定义空析构函数。改为写 =default
。
但是,在派生的 class 中使用 override
,并且 不要 在其中使用 virtual
。
我觉得很奇怪。请帮我解释一下。我有一个 class 在单独的线程中启动无限循环,还有两个 class 继承它。其中一个 classes 将要在外部触发的接口实现为 std::shared_ptr
,而另一个 class 将此接口保持为 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;
}
多态性在构建和销毁过程中并不像您预期的那样起作用。当前类型是仍然存在的最派生类型。当您处于 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
对象在一秒后被销毁,此时您会看到输出发生变化,表明被调用的虚函数在此时发生了变化。
我不确定此代码是否有效,或者在执行其成员函数之一时破坏对象的 derived
部分是否是未定义的行为。
您遇到了一个问题,因为您假设生成的线程不会立即启动,并且当前线程在执行任何操作之前有时间初始化当前对象的状态。
这不成立导致两个问题。
- 您访问了当前对象中尚未初始化的状态。
- 您使用的多态函数在对象完全构建之前不能保证有效。
您在析构函数中做了一个小假设:
- 您从没有虚拟析构函数的对象继承。
- 在对象开始销毁后,您的线程可能仍会访问状态。如果确实如此(访问被破坏)那么它就是 UB。您的线程需要能够检查当前对象状态是否有效(即所有派生 类 必须锁定
run
并确保其状态为真并且所有析构函数必须设置run
为假。
你的问题出在这里:
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::thread 成为对象的成员,并强制所有线程保持状态,直到状态正确初始化(在创建对象时)。
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();
除了 François Andrieux 指出的内容之外,您真正的问题是您在构造完成之前使用 this
对象启动线程 运行。它可能会也可能不会看到构造的派生类型,具体取决于时间。
它并没有像他暗示的那样从 构造函数调用thread_fun
。它在不同的线程上调用它,在未来某个未知的时间点。它可能发生在这个基础 class 构造函数返回之前的不同核心上,或者在派生 class 的构造过程中的任何其他随机点,或者更晚。
在对象准备好使用之前,您无法安全地启动线程的功能。
将创作与制作分开go。这是最简单的事情了。
同时
virtual ~Signal(){}
不要定义空析构函数。改为写 =default
。
但是,在派生的 class 中使用 override
,并且 不要 在其中使用 virtual
。