【问题标题】:Should destructors be threadsafe?析构函数应该是线程安全的吗?
【发布时间】:2010-10-13 09:13:06
【问题描述】:

我正在查看遗留代码,发现以下 sn-p:

MyClass::~MyClass()
{
   EnterCriticalSection(&cs);

//Access Data Members, **NO Global** members are being accessed here


  LeaveCriticalSection(&cs);
}

我想知道是否有任何机会保护析构函数?

考虑一个场景:

1. Thread1 - About to execute any of the member function which uses critical section
2. Thread2-  About to execute destructor.

如果执行顺序是 1=>2,那么它可能会起作用。但是如果顺序颠倒了呢?

这是设计问题吗?

【问题讨论】:

    标签: c++ multithreading destructor


    【解决方案1】:

    在使用对象时不应调用析构函数。如果您正在处理这样的情况,需要从根本上解决问题。然而,析构函数可能想要改变一些其他的东西(这与被析构的类无关)并且它可能需要一个临界区(例如减少一个 global 计数器)。

    【讨论】:

    • 但是它本身应该是线程安全的吗?一个类是否有可能被并行“销毁两次”?
    • @TomášZat .
    • 尽管答案很老,但我不同意。如果成员变量已由另一个线程而不是调用 dtor 的线程修改,则必须应用同步。不过,它不需要是“关键部分”。需要的是,两个线程之间存在“Synchronises-With”关系。这可能需要在任一线程中使用适当的内存屏障 - 除非线程相同。
    【解决方案2】:

    我认为你有一个更根本的问题。当另一个线程仍在调用成员函数时,在一个线程上销毁您的对象是不合法的。这本身就是错误的。

    即使你成功地用临界区保护了你的析构函数,当另一个线程开始执行函数的剩余部分时会发生什么?它将在一个已删除的对象上执行此操作,该对象(取决于它的分配位置)将是垃圾内存或简单的无效对象。

    您需要更改代码以确保对象在仍在使用时不会被破坏。

    【讨论】:

      【解决方案3】:

      定义“线程安全”。这可能是现代计算中最难理解的两个词。

      但是,如果有可能从两个不同的线程中两次输入析构函数(正如同步对象的使用所暗示的那样),那么您的代码就处于很深的 doo-doo 中。正在删除您所询问的对象的对象应该对此进行管理-(可能)应该在该级别进行同步。

      【讨论】:

        【解决方案4】:

        不会有所作为。如果,正如你所说,调用的顺序是相反的,那么你正在调用一个被破坏的对象的成员函数,这将失败。同步无法修复该逻辑错误(对于初学者来说,成员函数调用将尝试获取已被破坏的锁对象)。

        【讨论】:

          【解决方案5】:

          如果您要访问全局变量,您可能需要线程安全,是的

          例如。我的“Window”类将自己添加到构造函数中的列表“knownWindows”中,并在析构函数中将自己删除。 “knownWindows”需要是线程安全的,因此它们在执行此操作时都会锁定互斥锁。

          另一方面,如果您的析构函数只访问被销毁对象的成员,那么您就有了设计问题。

          【讨论】:

            【解决方案6】:

            我赞同 Neil ButterWorth 的评论。当然,负责删除和访问 myclass 的实体应该对此进行检查。

            这种同步实际上是从创建 MyClass 类型的对象的那一刻开始的。

            【讨论】:

              【解决方案7】:

              我见过一个 ACE 线程的案例,其中线程在 ACE_Task_Base 对象上运行,而该对象被另一个线程销毁。析构函数在等待条件之前获取锁并通知包含的线程。在 ACE_Task_Base 信号上运行的线程在退出时发出条件信号并让析构函数完成并退出第一个线程:

              class PeriodicThread : public ACE_Task_Base
              {
              public:
                 PeriodicThread() : exit_( false ), mutex_()
                 {
                 }
                 ~PeriodicThread()
                 {
                    mutex_.acquire();
                    exit_ = true;
                    mutex_.release();
                    wait(); // wait for running thread to exit
                 }
                 int svc()
                 {
                    mutex_.acquire();
                    while ( !exit_ ) { 
                       mutex_.release();
                       // perform periodic operation
                       mutex_.acquire();
                    }
                    mutex_.release();
                 }
              private:
                 bool exit_;
                 ACE_Thread_Mutex mutex_;
              };
              

              在这段代码中,析构函数必须使用线程安全技术来保证对象在运行svc()的线程退出之前不会被销毁。

              【讨论】:

                【解决方案8】:

                您的 cmets 说“没有全球成员在此处被访问”,所以我猜不会。只有创建对象的线程才能销毁它,那么您会从哪个线程保护它?

                我自己喜欢有序的创建和销毁,其中只有一个对象拥有另一个子对象,而引用该子对象的任何其他对象都是树中更靠下的后代。如果这些子对象中的任何一个代表不同的线程,那么它们将确保在销毁继续向上树之前完成。

                例子:

                • main() 创建对象 A
                  • 对象 A 包含对象 B
                    • 对象 B 包含对象 C
                      • 对象 C 创建一个访问对象 A 和 B 的线程
                      • 对象 C 的析构函数运行,等待其线程完成
                    • 对象 B 的析构函数运行
                  • 对象 A 的析构函数运行
                • main() 返回

                对象 A 和 B 的析构函数根本不需要考虑线程,对象 C 的析构函数只需要与它选择创建自己的线程实现一些通信机制(例如等待事件)。

                如果您开始将对象的引用(指针)分发给任意线程而不跟踪这些线程的创建和销毁时间,那么您只会遇到麻烦,但是如果您这样做,那么您应该使用引用计数,如果你是,那么在调用析构函数时为时已晚。如果仍然有一个对象的引用,那么没有人应该尝试调用它的析构函数。

                【讨论】:

                  【解决方案9】:

                  老问题,但恕我直言。

                  一般来说,更改临界区的类的公共成员(可从不同线程访问)应该锁定该临界区。但是对象的销毁是对象状态的最终改变包括临界区。

                  因此,如果正在进行异步操作,进入对象的这个临界状态,销毁肯定应该等到该临界区再次离开。一种方法是在析构函数中使用锁定。当然,这无助于保证以后不会再错误地访问对象本身。

                  但该技术可用于同步。在异步结束时销毁对象。关键部分的操作。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2023-03-30
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多