【问题标题】:prevent derived class from being destroyed防止派生类被破坏
【发布时间】:2014-01-09 21:15:56
【问题描述】:

我有一个带有受保护构造函数的基类 IRunnable,因此不能创建基类的任何实例。该类有一个纯虚方法run(),它必须由派生类实现。我将 IRunnable * 的指针传递给某种类似 java 的 Executor,它控制多个线程,并将这些 Runnables 分配给它们。 我的问题是,当这样一个 IRunnable * 指针指向 IRunnable 派生类的对象时,它位于另一个线程的堆栈上,被分配给工作线程时,我不能确定派生对象不是在工作线程仍在使用它时被销毁,因为在其线程上拥有对象的线程可能会离开创建对象的范围。 例如:

int main(void)
{
    CExecutor myExecutor(...); 
    for (UInt32 i = 0; i < 2; i++)
    {
        derivedRunnable myRunnable1; //will be destroyed after each loop iteration
        myExecutor.submit(myRunnable1);
    }
}

class derivedRunnable : public IRunnable
{
public:
    derivedRunnable(const char * name = NULL) : IRunnable(name) {}
    ~derivedRunnable() {}
    void run(void)
    {
        for (UInt32 i = 0; i < 100; i++)
        {
            char name [256] = {"\0"};

            pthread_getname_np(pthread_self(), name, 255);
            printf("%s in step %d\n", name, i);
        }
        fflush(stdout);
    }
};

我在基类 IRunnable 中实现了一个引用计数,并且我在析构函数中执行了一个阻塞调用,该调用只有在使用它的最后一个线程向它注销时才会返回。问题是,派生类 get 的析构函数首先被调用,因此在调用阻塞调用破坏的基类之前,对象将被部分破坏。 在上面的示例中,我收到以下运行时错误:
pure virtual method called
terminate called without an active exception 如果我在 .submit() 调用之后插入一些 usec 的 usleep ,它将起作用,因为线程将在它被销毁之前完成可运行对象

class IRunnable
{
    friend class CThread;
    friend class CExecutor;
private:
    CMutex mutx;
    CBinarySemaphore sem;
    UInt32 userCount;
    [...]
    virtual void run(void) = 0;
    IRunnable(const IRunnable & rhs); //deny
    IRunnable & operator= (const IRunnable & rhs); //deny

    void registerUser(void)
    {
        mutx.take(true);
        if (0 == userCount++)
            sem.take(true);
        mutx.give();
    }

    void unregisterUser(void)
    {
        mutx.take(true);
        if (0 == --userCount)
            sem.give();
        mutx.give();
    }

protected:
    IRunnable(const char * n = NULL)
    :mutx(true,false)
    ,sem(true,false)
    ,userCount(0)
    {
        setName(n);
    }

    ~IRunnable()
    {
        sem.take(true);
    }
    [...]

我能做什么?

【问题讨论】:

    标签: c++ multithreading inheritance


    【解决方案1】:

    IRunnable 需要有一个虚拟析构函数,以便以正确的顺序调用正确的析构函数。

    【讨论】:

    • 虽然我其实不是100%确定虚拟析构函数的作用是什么,但我试了一下,问题依旧
    • 这个问题回答了为什么。具体为什么你的代码仍然崩溃我不知道。我浏览了一下,注意到你在派生其他类的类中没有虚拟解构器。 stackoverflow.com/questions/461203/…
    • 从您的链接中,我发现一条评论指出“虚拟”确保破坏链从顶部而不是底部开始。我认为这将解决我的问题。我添加了一个 printf 做基类和派生类的析构函数,以查看它们被调用的顺序,但无论是否虚拟,顺序始终是 1. 派生 2. 基 - 我做错了吗?
    • 将首先调用派生析构函数,然后调用基类,因为基类必须在派生类位仍然存在时保持有效。因此,要使您的信号量起作用,我想它需要在派生类的析构函数中。
    【解决方案2】:

    如果你在析构函数中阻塞,那么你的提交循环不会是异步的,这就是多线程的重点。生命周期必须继续超过分配范围。为此,您应该动态分配您的 IRunnable 对象,并让您的 Executor 获得所有权。作业完成后,Executor 将负责将其删除。

    更好的是,为此接口使用共享指针。这样调用线程仍然可以引用该对象(例如检查完成)。最后一个线程完成将删除该对象。

    编辑:添加示例代码:

    for (UInt32 i = 0; i < 2; i++)
    {
        shared_ptr< derivedRunnable > myRunnable1(new derivedRunnable);
        myExecutor.submit(myRunnable1);
    }
    

    或保存任务:

    list < shared_ptr < derivedRunnable > > jobs;
    for (UInt32 i = 0; i < 2; i++)
    {
        shared_ptr< derivedRunnable > myRunnable(new derivedRunnable);
        myExecutor.submit(myRunnable);
        jobs.push_back(myRunnable);
    }
    // do other stuff
    // call a status function that you haven't written yet:
    jobs.front()->WaitForCompletion();
    

    顺便说一句,您还应该考虑切换到 std::thread 或 boost::thread,这取决于您的编译器的年份。

    【讨论】:

    • 我知道在调用显式删除之前,分配在堆上的对象不会被销毁,但这并不能解决我的问题。共享指针到底是什么意思?
    • 为什么它不能解决你的问题?在工作人员完成之前不会删除对象?
    • 好吧,我宁愿有一个不依赖程序员仅使用动态分配的解决方案,我希望它也可以处理堆栈上的对象。您当然是对的,我的阻塞析构函数将使给定的示例同步,但这对我来说完全没问题,该示例只是展示错误发生的情况,而不是展示如何正确使用执行器的情况
    • 我得出的结论是,我故意想要拥有的东西根本不可能。堆分配和所有权传递,或使用 shared_pointers 是必要的,尽管我绝对不喜欢它。 Java 也这样做,它的垃圾收集器非常烦人,它会定期停止你的程序进行清理。
    【解决方案3】:

    您的问题是不清楚对象的所有权。从您的示例中可以清楚地看出该对象属于线程,因此您应该传递所有权,并且线程应该销毁它。为此,IRunnable 析构函数必须是公开的。其实没有理由不公开。

    啊!并且对象应该是动态创建的:

    derivedRunnable *myRunnable1 = new derivedRunnable();
    myExecutor.submit(myRunnable1);
    

    submit 函数,当然会接收一个指针并将其传递给线程。当线程用完可运行对象时,它会销毁它:

    void threadfunc(IRunnable *runnable)
    {
        //run it
        delete runnable;
    }
    

    还有很多更复杂的解决方案,但这是最简单的。如果可以选择 C++11,我最喜欢的是使用 std::unique_ptr&lt;IRunnable&gt;。这样对象的销毁是自动的。或者如果需要在主线程中保留runnable,可以使用std::shard_ptr&lt;IRunnable&gt;:自动销毁和共享所有权。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-10
      • 2020-03-05
      • 2016-10-14
      • 2017-05-29
      • 2011-03-23
      相关资源
      最近更新 更多