问题
I'm trying to get a class run a thread, which will call a virtual member function named Tick() in a loop. Then I tried to derive a class and override the base::Tick().
but when execute, the program just call the base class's Tick instead of override one. any solutions?
#include <iostream>
#include <atomic>
#include <thread>
#include <chrono>
using namespace std;
class Runnable {
public:
Runnable() : running_(ATOMIC_VAR_INIT(false)) {
}
~Runnable() {
if (running_)
thread_.join();
}
void Stop() {
if (std::atomic_exchange(&running_, false))
thread_.join();
}
void Start() {
if (!std::atomic_exchange(&running_, true)) {
thread_ = std::thread(&Runnable::Thread, this);
}
}
virtual void Tick() {
cout << "parent" << endl;
};
std::atomic<bool> running_;
private:
std::thread thread_;
static void Thread(Runnable *self) {
while(self->running_) {
self->Tick();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
};
class Fn : public Runnable {
public:
void Tick() {
cout << "children" << endl;
}
};
int main (int argc, char const* argv[])
{
Fn fn;
fn.Start();
return 0;
}
outputs:
parent
回答1:
You can't let an object run out of scope until you're finished using it! The return 0;
at the end of main
causes fn
to go out of scope. So by the time you get around to calling tick
, there's no guarantee the object even exists any more.
(The logic in ~Runnable
is totally broken. Inside the destructor is way too late -- the object is already at least partially destroyed.)
回答2:
The approach of using inheritance with the parent serving as control for the thread and the children implementing the functions is a bad idea in general. The common problems with this approach come from construction and destruction:
if the thread is started from the constructor in the parent (control) then it might start running before the constructor completes and the thread might call the virtual function before the complete object has been fully constructed
if the thread is stopped in the destructor of the parent, then by the time that the control joins the thread, the thread is executing a method on an object that does no longer exist.
In your particular case you are hitting the second case. The program starts executing, and in main
the second thread is started. At that point there is a race between the main thread and the newly launched, if the new thread is faster (unlikely, as starting the thread is an expensive operation), it will call the member method Tick
that will be dispatched to the final overrider Fn::Tick
.
But if the main thread is faster it will exit the scope of main
, and it will start destruction of the object, it will complete destruction of the Fn
object and during construction of the Runnable
it will join
the thread. If the main thread is fast enough, it will make it to the join
before the second thread and wait there for the second thread to call Tick
on the now final overrider that is Runnable::Tick
. Note that this is Undefined Behavior, and not guaranteed, since the second thread is accessing an object that is being destroyed.
Also, there are other possible orderings, like for example, the second thread could dispatch to Fn::Tick
before the main thread starts destruction, but might not complete the function before the main thread destroys the Fn
sub object, in which case your second thread would be calling a member function on a dead object.
You should rather follow the approach in the C++ standard: separate the control from the logic, fully construct the object that will be run and pass it to the thread during construction. Note that this is the case of Java's Runnable
, which is recommended over extending the Thread
class. Note that from a design point of view this separation makes sense: the thread object manages the execution, and the runnable is the code to execute.
A thread is not a ticker, but rather what controls the execution of the ticker. And in your code Runnable
is not something that can be run, but rather something that runs other objects that happen to derive from it.
来源:https://stackoverflow.com/questions/10634603/c11-thread-doesnt-work-with-virtual-member-function