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?