首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >使用共享指针从另一个线程进行纯虚拟调用

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

Stack Overflow用户
提问于 2021-10-07 16:34:42
回答 3查看 206关注 0票数 3

我觉得很奇怪。请帮我解释一下。我有一个类,它在一个单独的线程中启动无限循环,还有两个继承它的类。其中一个类实现了将作为std::shared_ptr在外部触发的接口,另一个类将此接口保存为std::weak_ptr。请看下面的代码。对不起,我的代码太多了,我只是想尽量简短地重现错误。为什么有时我会在Sender::notify函数中进行纯粹的虚拟调用?据我所知,std::shared_ptr是可重入的。

代码语言:javascript
复制
#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;
}
EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2021-10-07 16:44:13

多态并不像您在构建和破坏过程中所期望的那样工作。当前类型是仍然存在的派生类型最多的类型。当您在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对象在1秒后被销毁,此时您将看到输出的变化,这表明被调用的虚拟函数在那个点上发生了变化。

我不确定这段代码是否有效,或者在执行对象的一个成员函数时销毁对象的derived部分是否是未定义的行为。

票数 7
EN

Stack Overflow用户

发布于 2021-10-07 17:06:56

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

这是不成立的,这导致两个问题。

访问当前对象中尚未被initialized.

  • You的状态的
  1. 使用一个多态函数,直到该对象被完全构造后才能工作。

你在你的破坏者中做了一个小小的假设:

从没有虚拟destructor.

  • Your线程的对象继承的
  1. 可能在对象启动销毁后仍然访问状态。如果这样做了(访问被破坏),那么它就是UB。您的线程需要能够检查当前对象状态是否有效(也就是说,所有派生类都必须在run上获得一个锁,并确保其状态为true,并且所有析构函数都必须将run设置为false。

你的问题在于:

代码语言:javascript
复制
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::线程成为对象的成员,并强制所有线程保持到状态被正确初始化为止(在创建对象的位置)。

代码语言:javascript
复制
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中使用:

代码语言:javascript
复制
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();
票数 1
EN

Stack Overflow用户

发布于 2021-10-07 17:07:08

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

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

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

把造物和让它分开。这是最简单的事。

同时

代码语言:javascript
复制
    `virtual ~Signal(){}`

不要定义空析构函数。写=default代替。但是,在派生类中使用override,而在那里使用virtual

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/69484780

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档