Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Virtual call within thread ignores derived class

In the following program I have a virtual call from within a thread:

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>

class A {
public:
    virtual ~A() { t.join(); }
    virtual void getname() { std::cout << "I am A.\n"; }
    void printname() 
    { 
        std::unique_lock<std::mutex> lock{mtx};
        cv.wait(lock, [this]() {return ready_to_print; });
        getname(); 
    };
    void set_ready() { std::lock_guard<std::mutex> lock{mtx}; ready_to_print = true; cv.notify_one(); }
    void go() { t = std::thread{&A::printname,this}; };

    bool ready_to_print{false};
    std::condition_variable cv;
    std::mutex mtx;
    std::thread t{&A::printname,this};
};

class B : public A {
public:
    int x{4};
};

class C : public B {
    void getname() override { std::cout << "I am C.\n"; }
};

int main()
{
    C c;
    A* a{&c};
    a->getname();
    a->set_ready();
}

I was hoping the program would print:

I am C.
I am C.

But instead it prints:

I am C.
I am A.

In the program I wait until the derived object is fully constructed before I call the virtual member function. However the thread is started before the object is fully constructed.

How can the virtual call be assured?

like image 802
wally Avatar asked Dec 09 '16 00:12

wally


1 Answers

The shown code exhibits a race condition, and undefined behavior.

In your main():

C c;

// ...

a->set_ready();

Immediately, after set_ready() returns, execution thread leaves main(). This results in immediate destruction of c, starting with the superclass C, and continuing to destroying B, then A.

c is declared in automatic scope. That means that as soon as main() returns, it's gone. Joined the choir invisible. It is no more. It ceased to exist. It's an ex-object.

Your join() is in the superclass's destructor. Nothing stops C from being destroyed. The destructor will only pause and wait to join the thread, when the superclass gets destroyed, but C will start getting destroyed immediately!

Once the C superclass is destroyed, its virtual method no longer exists, and invoking the virtual function will end up executing the virtual function in the base class.

Meanwhile the other execution thread is waiting on the mutex and the condition variable. The race condition is that you have no guarantee that the other execution thread will wake up and start executing before the parent thread destroys C, which it does immediately after signaling the condition variable.

All that signaling the condition variable gives you is that whatever execution thread is spinning on the condition variable, that execution thread will start executing. Eventually. That thread could, on a very loaded server, start executing seconds later, after its signaled via the condition variable. Its object is gone a long time ago. It was in automatic scope, and main() destroyed it (or, rather, the C subclass is already destroyed, and A's destructor is waiting to join the thread).

The behavior you are observing is the parent thread managing to destroy the C superclass before the std::thread gets around to making its virtual method call, after receiving the signal from the condition variable, and unlocking its mutex.

That's the race condition.

Furthermore, executing a virtual method call at the same time the virtual object is being destroyed is already a non-starter. It's undefined behavior. Even if the execution thread ends up in the overridden method, its object is being destroyed by another thread at the same time. You're pretty much screwed, at that point, no matter which way you turn.

Lessons learn: rigging up a std::thread to execute something in this object is a minefield of undefined behavior. There are ways of doing it correctly, but it's hard.

like image 72
Sam Varshavchik Avatar answered Sep 29 '22 06:09

Sam Varshavchik