Почему методы родителя остаются живыми после уничтожения ребенка - PullRequest
2 голосов
/ 13 июня 2019

Я не понимаю, почему запускается функция '' 'execute' '' класса Parent.Я чувствую, что есть два экземпляра: один для родительского класса, другой для дочернего класса, но почему?Действительно, эта программа печатает «1 родитель», как я и ожидал, «1 ребенок» или «0 родитель».Если я раскомментирую строку задержки, вывод будет «1 дочерний».

Я знаю, что в этой программе есть условие гонки.Эта программа была создана только для понимания принципа работы наследования в многопоточной среде.

Спасибо!

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <string>
#include <thread>

class Parent
{
public:
    std::thread myThread;
    int a;
    Parent() {
        this->myThread = std::thread();
        this->a = 0;
    }
    void start()
    {
        this->myThread = std::thread(&Parent::execute, this);
    }
    virtual void execute() {
        std::cout << a << " Parent" << std::endl;
    }
    virtual ~Parent() {
        while(!this->myThread.joinable());
        this->myThread.join();
    }

};

class Child : public Parent
{
public:
    Child() {
        this->a = 1;
    }
    void execute() override {
        std::cout << a << " Child" << std::endl;
    }
    ~Child() {

    }

};

int main()
{
    std::cout << "Init" << std::endl;
    Child * chld = new Child();
    chld->start();
    //std::this_thread::sleep_for(std::chrono::milliseconds(x));
    std::cout << "Delete" << std::endl;
    delete chld;
    return 0;
}

Ответы [ 3 ]

4 голосов
/ 13 июня 2019

Ваша программа имеет неопределенное поведение, что означает "все может случиться".

Вы запускаете новый поток, который содержит указатель (this) на объект.Этот поток позже вызовет виртуальный метод, что означает, что ему нужно использовать данные из объекта, на который он указывает.Указатель vtable сам по себе является своего рода данными класса.Поскольку вы удаляете свой объект из другого потока, указатель (this) просто указывает на разрушенный объект, и доступ к данным (vtable) из удаленного объекта является неопределенным поведением.

Ваше наблюдение зависит от реализации компилятораи, возможно, также на уровне оптимизации.Возможно, во время деконструкции ваш компилятор перематывает указатель vtable вниз на указатель базового класса.А так как память объекта не переопределена никаким другим контентом (который даже не определен!), Вы можете наблюдать вызов базовой функции после уничтожения.Но это не то, на что вы можете положиться, поскольку вообще не разрешается использовать какой-либо объект после уничтожения, ЕСЛИ вы используете члены-данные объекта, который здесь является указателем vtable.

Вкратце: ваш код содержитошибка, и все может произойти, поскольку это неопределенное поведение.

2 голосов
/ 13 июня 2019

Ваш код демонстрирует неопределенное поведение (в вашем случае это приводит к вызову Parent::execute) из-за состояния гонки между созданием потока и Child уничтожением объекта. Чтобы исправить это, вы можете определить надлежащие методы start и stop в вашем классе Parent и вызвать stop в деструкторе Child, чтобы предотвратить его уничтожение до присоединения потока.

class Parent
{
public:
    Parent(): myThread_() {
        std::cout << "Parent CTor" << std::endl;
    }
    virtual ~Parent() = default;
    bool start()
    {
        std::cout << "start" << std::endl;
        if (myThread_.joinable()) {
            std::cout << "already started" << std::endl;
            return false;
        }
        myThread_ = std::thread([this]() {
            execute();
        });
        return true;
    }
    bool stop() {
        std::cout << "stop" << std::endl;
        if (!myThread_.joinable()) {
            std::cout << "not started" << std::endl;
            return false;
        }
        myThread_.join();
        return true;
    }
    virtual void execute() = 0;

private:
    std::thread myThread_;
};

class Child : public Parent
{
public:
    Child() {
        std::cout << "Child CTor" << std::endl;
    }
    ~Child() override {
        stop();
    }
    void execute() override {
        std::cout << "Child::execute()" << std::endl;
    }
};

int main()
{
    std::cout << "Init" << std::endl;
    Child * chld = new Child();
    chld->start();
    std::cout << "Delete" << std::endl;
    delete chld;
    return 0;
}

Я определил Parent::execute как абстрактный, потому что, вероятно, вы вообще не хотите, чтобы он вызывался, а в случае другой ошибки, по крайней мере, вы можете получить

terminate, pure virtual method called
2 голосов
/ 13 июня 2019

Это ничего общего с потоками. Вы можете воспроизвести все это, включая неопределенное поведение, синхронно.

Однопоточная версия ваших классов:

#include <iostream>
#include <string>

class Parent
{
public:
    int a;
    Parent() : a(0) {}
    virtual ~Parent() {}

    virtual void execute() {
        std::cout << a << " Parent" << std::endl;
    }
};

class Child : public Parent
{
public:
    Child() {
        a = 1;
    }
    void execute() override {
        std::cout << a << " Child" << std::endl;
    }
};

и однопоточные тестовые примеры, демонстрирующие точно такое же поведение:

int main()
{
    Child c;

    std::cout << "=== automatic lifetime ===\n";
    std::cout << "virtual dispatch: ";
    c.execute();
    std::cout << "explicit static dispatch: ";
    c.Parent::execute();

    std::cout << "=== dynamic lifetime ===\n";
    Child *pc = new Child;
    std::cout << "virtual dispatch: ";
    pc->execute();
    std::cout << "explicit static dispatch: ";
    pc->Parent::execute();

    std::cout << "=== undefined behaviour ===\n";
    delete pc;
    std::cout << "explicit static dispatch: ";
    pc->Parent::execute();
    std::cout << "virtual dispatch: ";
    pc->execute();
}

Последние два выходных оператора поменялись местами, потому что последний из них потерпел крах, когда я его запустил (предпоследний по-прежнему UB, но случайно не вылетел)

=== automatic lifetime ===
virtual dispatch: 1 Child
explicit static dispatch: 1 Parent
=== dynamic lifetime ===
virtual dispatch: 1 Child
explicit static dispatch: 1 Parent
=== undefined behaviour ===
explicit static dispatch: 1 Parent
Segmentation fault      (core dumped) ./a.out
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...