I just got a juicy race condition. Consider the following classes:
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
class A
{
std::thread th;
std::atomic_bool stop = false;
public:
A() = default;
A(const A &) = delete;
A &operator=(const A &) = delete;
~A()
{
stop.store(true);
th.join();
}
virtual void Step() = 0;
void Start()
{
th = std::thread([this]
{
while (true)
{
if (stop.load())
return;
// Just to help reproduce the race condition.
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Step();
}
});
}
};
struct B : A
{
void Step() override
{
std::cout << "Step!" << std::endl;
}
};
int main()
{
B b;
b.Start();
// Just to help reproduce the race condition.
std::this_thread::sleep_for(std::chrono::milliseconds(110));
}
This crashes due to a pure virtual function call (if we ignore the data race UB).
Right before we enter the body of ~A()
, before we can stop the thread, the vtable pointer is changed to point to A
's vtable, so the next call of Step()
crashes, now being a pure virtual call.
This can be fixed by stopping the thread in ~B()
, but that means every derived class must remember to do it, or bad things will happen.
I wish I could design A
to take care of this by itself. Is it possible?
virtual void Step()
fromA
to an interfaceIJob
and take astd::unique<IJob>
. Alternatively, use CRTP... – CompletionIJob
sounds like a good idea. I considered CRTP, but I think it's going to suffer the same problem? – Bomarthis
inStart()
? SinceStep()
works in destructor, it should be stateless? – Staggvoid A::Step() {}
, then there will not be crash due to pure virtual function call. – WiegandStep()
stateless, I'd prefer derived classes to not have to think about it, and just be able to put anything in there. – BomarB::Step()
accesses fields ofB
then the thread must be stopped before running~B()
. So it shall be a responsibility of child class. – WiegandA::Step()
should be something likevoid Step(){ m_callback() }
. Wherem_callback
is a function pointer orstd::function
. All the derived class should set the callback before executes the thread or derived destructor executes. Then the callback itself should only capture variables that lifetime is longer than derived class. Though it could only be taken care by the writer of derived class... – StaggITeardown
to take care of this case before destructor. likevoid tearDown(){ stop.store(true); th.join();}
That makes the design simpler without dealing with object lifetimes.. – Stagg