使用共享指针从另一个线程进行纯虚拟调用

Pure virtual call from another thread using shared pointer

我觉得很奇怪。请帮我解释一下。我有一个 class 在单独的线程中启动无限循环,还有两个 class 继承它。其中一个 classes 将要在外部触发的接口实现为 std::shared_ptr,而另一个 class 将此接口保持为 std::weak_ptr。请看下面的代码。很抱歉有很多代码,我试图尽可能简短地重现错误。为什么有时我在 Sender::notify 函数中进行纯虚拟调用?据我所知 std::shared_ptr 是可重入的。

#include <iostream>
#include <memory>
#include <thread>
#include <atomic>
#include <list>
#include <mutex>


class Thread : private std::thread {
    std::atomic_bool run {true};
public:
    Thread() : std::thread([this](){ thread_fun(); }) {}

    void thread_fun() {
        while (run) loop_iteration();
    }

    virtual void loop_iteration() = 0;

    virtual ~Thread() {
        run.exchange(false);
        join();
        std::cout << "Thread released." << std::endl;
    }
};

class Sender : public Thread {
public:
    class Signal{
    public:
        virtual void send() = 0;
        virtual ~Signal(){}
    };

    void add_receiver(std::weak_ptr<Signal> receiver) {
        std::lock_guard<std::mutex> lock(receivers_mutex);
        receivers.push_back(receiver);
    }

    void notify() {
        std::lock_guard<std::mutex> lock(receivers_mutex);
        for (auto r : receivers)
            if (auto shp = r.lock())
                shp->send(); //Somethimes I get the pure virtual call here
    }

private:
    std::mutex receivers_mutex;
    std::list<std::weak_ptr<Signal>> receivers;

    void loop_iteration() override {
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        notify();
    }
};

class Receiver : public Thread, public Sender::Signal {
    std::atomic_bool notified {false};

public:
    void send() override {
        notified.exchange(true);
    }

private:
    void loop_iteration() override {
        std::this_thread::sleep_for(std::chrono::milliseconds(250));
        std::cout << "This thread was " << (notified? " " : "not ") << "notified" << std::endl;
    }
};


int main() {
   std::shared_ptr<Thread>
           receiver = std::make_shared<Receiver>(),
           notifier = std::make_shared<Sender>();

   std::dynamic_pointer_cast<Sender>(notifier)->add_receiver(
               std::dynamic_pointer_cast<Sender::Signal>(receiver));

   receiver.reset();

   notifier.reset();

   return 0;
}

多态性在构建和销毁过程中并不像您预期​​的那样起作用。当前类型是仍然存在的最派生类型。当您处于 Thread::~Thread 时,您的对象的 Sender 部分已经被完全销毁,因此调用它的覆盖是不安全的。

thread_fun在构造函数完成之前或析构函数开始之后尝试运行loop_iterator()时,它不会多态调度,而是调用Thread::loop_iteration是纯虚函数(= 0).

https://en.cppreference.com/w/cpp/language/virtual#During_construction_and_destruction

这是一个演示:https://godbolt.org/z/4vsPGYq97

derived 对象在一秒后被销毁,此时您会看到输出发生变化,表明被调用的虚函数在此时发生了变化。

我不确定此代码是否有效,或者在执行其成员函数之一时破坏对象的 derived 部分是否是未定义的行为。

您遇到了一个问题,因为您假设生成的线程不会立即启动,并且当前线程在执行任何操作之前有时间初始化当前对象的状态。

这不成立导致两个问题。

  1. 您访问了当前对象中尚未初始化的状态。
  2. 您使用的多态函数在对象完全构建之前不能保证有效。

您在析构函数中做了一个小假设:

  1. 您从没有虚拟析构函数的对象继承。
  2. 在对象开始销毁后,您的线程可能仍会访问状态。如果确实如此(访问被破坏)那么它就是 UB。您的线程需要能够检查当前对象状态是否有效(即所有派生 类 必须锁定 run 并确保其状态为真并且所有析构函数必须设置 run 为假。

你的问题出在这里:

class Thread : private std::thread {
    std::atomic_bool run {true};
public:
    Thread()
        // Here you are starting a separate thread of execution
        // That calls the method thread_fun on the current object.
        //
        // No problem so far. BUT you should note that "this" object
        // is not fully constructed at this point and there is no
        // guarantees that the thread you just started will wait
        // for this thread to finish before doing anything.
        : std::thread([this](){ thread_fun(); })
    {}

    void thread_fun() {
        // The new thread has just started to run.
        // And is now accessing the variable `run`.
        //
        // But `run` is a member and initialized after
        // the base class so you have no idea if the parent
        // thread has correctly initialized this variable yet.
        //
        // There is no guratnee that the parent will get to 
        // the initialization point of `run` before this new thread
        // gets to this point where it is using it.
        while (run) {

            // Here you are calling a virtual function.
            // The trouble is that virtual functions are not
            // guranteed to work as you would expect until all the
            // constructors of the object have run.
            //   i.e. from base all the way to most derived.
            //
            // So you not only have to wait for this base class to
            // be full constructed you must wait until the object
            // is full constructed before you call this method.
            loop_iteration();
        }
    }

    virtual void loop_iteration() = 0;

    virtual ~Thread() {
        // You have a problem in that std::thread destructor
        // is not virtual so you will not always call its destructor
        // correctly.
        //
        // But lets assume it was called correctly.
        // When you get to this point you have destroyed the
        // the state of all derived parts of your object.
        // So the function your thread is running better
        // not touch any of that state as it is not all invalid
        // and doing so is UB.
        //
        // If your object had no state then you are fine.

        run.exchange(false);

        join();
        std::cout << "Thread released." << std::endl;
    }
};

我认为更好的解决方案是使 std::thread 成为对象的成员,并强制所有线程保持状态,直到状态正确初始化(在创建对象时)。

class Thread {
    std::atomic_bool run;
    std::thread      thread;
public:
    Thread(std::function<void>& hold)
        // Make sure run is initialized before the thread.
        : run{false}
        , thread([this, &hold](){ thread_fun(hold); })
    {}

    void thread_fun(std::function<void>& hold) {

        // Pass in a hold function.
        // The creator of your objects defines this
        // It is supposed to make objects created until you
        // have all the state correctly set up.
        // once it is you allow any threads that have called
        // hold to be released so they can execute.
        hold();
        
        while (run) loop_iteration();
    }

    virtual void loop_iteration() = 0;

    virtual ~Thread() {
        run.exchange(false);
        join();
        std::cout << "Thread released." << std::endl;
    }

};

然后你可以创建一个简单的屏障以在 hold 中使用:

class Barrier
{
    bool                        threadsShouldWait = true;
    std::conditional_variable   cond;
    std::mutex                  mutex;

    void wait() {
         std::unique_lock<std::mutex> lock(mutex);
         cond.wait([&](){return !threadsShouldWait;}, lock);
    }
    void release() {
         std::unique_lock<std::mutex> lock(mutex);
         threadsShouldWait = false;
         cond.notify_all();
    }
}

int main()
{
   // Note you don't need to use the same barrier for
   // both these variables. I am just illustrating one use case.
   Barrier   barrier;

   std::shared_ptr<Thread> receiver = std::make_shared<Receiver>([&barrier](){barrier.wait();});
   std::shared_ptr<Thread> notifier = std::make_shared<Sender>([&barrier](){barrier.wait();});

   barrier.release();

除了 François Andrieux 指出的内容之外,您真正的问题是您在构造完成之前使用 this 对象启动线程 运行。它可能会也可能不会看到构造的派生类型,具体取决于时间。

它并没有像他暗示的那样从 构造函数调用thread_fun 。它在不同的线程上调用它,在未来某个未知的时间点。它可能发生在这个基础 class 构造函数返回之前的不同核心上,或者在派生 class 的构造过程中的任何其他随机点,或者更晚。

在对象准备好使用之前,您无法安全地启动线程的功能。

将创作与制作分开go。这是最简单的事情了。

同时

virtual ~Signal(){}
不要定义空析构函数。改为写 =default。 但是,在派生的 class 中使用 override,并且 不要 在其中使用 virtual