【问题标题】:C++: Concurrency and destructorsC++:并发和析构函数
【发布时间】:2011-10-06 07:27:46
【问题描述】:

假设你有一个可以被许多线程访问的对象。临界区用于保护敏感区域。但是析构函数呢?就算我一进入析构函数就进入临界区,一旦析构函数被调用,对象是不是已经失效了?

我的思路:假设我进入析构函数,我必须等待临界区,因为其他线程仍在使用它。一旦他完成了,我就可以完成对对象的破坏。这有意义吗?

【问题讨论】:

  • 为什么仍然有线程在访问它,你为什么要销毁它?

标签: c++ concurrency destructor critical-section


【解决方案1】:

一般来说,你不应该销毁一个对象,直到你知道没有其他线程正在使用它。期间。

根据您的“思路”考虑这种情况:

  • 线程 A:获取对象 X 引用
  • 线程 A:锁定对象 X
  • 线程 B:获取对象 X 引用
  • 线程 B:对象 X 锁上的阻塞
  • 线程 A:解锁对象 X
  • 线程 B:锁定对象 X;解锁对象 X;销毁对象 X

现在考虑如果时间稍有不同会发生什么:

  • 线程 A:获取对象 X 引用
  • 线程 B:获取对象 X 引用
  • 线程 B:锁定对象 X;解锁对象 X;销毁对象 X
  • 线程 A:锁定对象 X - 崩溃

简而言之,对象销毁必须在对象本身以外的其他地方同步。一种常见的选择是使用引用计数。线程 A 将锁定对象引用本身,防止引用被删除和对象被销毁,直到它设法增加引用计数(保持对象存活)。然后线程 B 只是清除引用并减少引用计数。你无法预测哪个线程会真正调用析构函数,但无论如何它都是安全的。

引用计数模型可以通过boost::shared_ptrstd::shared_ptr轻松实现;除非所有线程中的所有shared_ptrs 都已被销毁(或指向其他位置),否则析构函数将不会运行,因此在销毁时您知道指向剩余对象的唯一指针是析构函数的this 指针自己。

请注意,在使用 shared_ptr 时,重要的是要防止原始对象引用发生更改,直到您可以捕获它的副本。例如:

std::shared_ptr<SomeObject> objref;
Mutex objlock;

void ok1() {
  objlock.lock();
  objref->dosomething(); // ok; reference is locked
  objlock.unlock();
}

void ok2() {
  std::shared_ptr<SomeObject> localref;
  objlock.lock();
  localref = objref;
  objlock.unlock();

  localref->dosomething(); // ok; local reference
}

void notok1() {
  objref->dosomething(); // not ok; reference may be modified
}

void notok2() {
  std::shared_ptr<SomeObject> localref = objref; // not ok; objref may be modified
  localref->dosomething();
}

请注意,同时读取 shared_ptr 是安全的,因此如果对您的应用程序有意义,您可以选择使用读写锁。

【讨论】:

  • 很好的答案!一直听说shared_ptr,我现在就开始用吧
  • 这个答案有一些很好的建议,可以在某些情况下使用,但通常是错误的。 “对象销毁必须在对象本身以外的其他地方同步”——这是不正确的。反驳该规则的反例是std::condition_variable;特别允许客户端在其他线程调用 wait() 时调用析构函数。
【解决方案2】:

如果一个对象正在使用中,那么你应该确保在对象的使用结束之前没有调用对象的析构函数。如果这是您的行为,那么它是一个潜在的问题,确实需要修复。

您应该确保如果一个线程正在销毁您的对象,那么另一个线程不应该调用该对象上的函数,或者第一个线程应该等到第二个线程完成函数调用。

是的,即使是析构函数也可能需要临界区来保护更新一些与类本身无关的全局数据。

【讨论】:

  • 这个答案并不适用于所有类:我认为一般来说你应该这样对待大多数类,但肯定有一些完美的类支持在使用对象之前调用析构函数结束。”
【解决方案3】:

有可能当一个线程在析构函数中等待 CS 时,另一个线程正在销毁对象,如果 CS 属于对象,它也会被销毁。所以这不是一个好的设计。

【讨论】:

    【解决方案4】:

    您绝对肯定需要确保您的对象生命周期小于消费者线程,否则您将面临一些严重的问题。要么:

    1. 使对象的消费者成为子对象,这样它们就不可能存在于您的对象之外,或者
    2. 使用消息传递/代理。

    如果你走后者路线,我强烈推荐 0mq http://www.zeromq.org/

    【讨论】:

      【解决方案5】:

      是的,当你在析构函数中时,对象已经失效了。

      我使用 Destroy() 方法进入临界区,然后自行销毁。

      Lifetime of object is over before destructor is called?

      【讨论】:

        【解决方案6】:

        是的,这样做很好。 如果一个类支持这样的使用,客户端不需要同步销毁;即他们不需要确保对象上的所有其他方法在调用析构函数之前完成。

        我建议客户不要假设他们可以做到这一点,除非明确记录在案。默认情况下,客户端确实有这种负担,特别是标准库对象(第 17.6.4.10/2 节)。

        不过,在某些情况下它是可以的;例如,std::condition_variable 的析构函数特别允许在~condition_variable() 启动时进行持续的condition_variable::wait() 方法调用。它只要求客户端在 ~condition_variable() 启动之后不要发起对 wait() 的调用。

        要求客户端同步对析构函数和构造函数的访问可能会更简洁,就像标准库的大多数其余部分一样。如果可行,我会建议这样做。

        但是,在某些模式下,减轻客户端完全同步销毁的负担可能是有意义的。 condition_variable 的整体模式似乎是这样的:考虑使用处理可能长时间运行的请求的对象。用户执行以下操作:

        1. 构造对象
        2. 使对象接收其他线程的请求。
        3. 导致对象停止接收请求:此时,一些未完成的请求可能正在进行,但无法调用新的请求。
        4. 销毁对象。析构函数将阻塞直到所有请求都完成,否则正在进行的请求可能会很糟糕。

        另一种方法是要求客户端确实需要同步访问。您可以想象上面的步骤 3.5,客户端在执行阻塞的对象上调用 shutdown() 方法,之后客户端可以安全地销毁该对象。但是,这种设计有一些缺点;它使 API 复杂化,并为 shutdown-but-valid 对象引入了额外的状态。

        考虑也许让步骤 (3) 阻塞,直到所有请求都完成。有权衡...

        【讨论】:

        • 您将遵循哪些标准来决定是让客户端同步销毁还是自行同步?
        • @dario_ramos – 我能想到的标准是:客户端的简单性,例如要考虑的其他状态,要忘记的其他功能等;析构函数的简单性,即如果它不必阻塞就很好;而且我认为如果其他一切都相同,您最好像标准库那样做只是为了保持一致性,即要求客户端同步。还有其他人吗?所以——如果你可以避免阻塞在 dtor 中而不给客户端带来任何负担,我猜就不要阻塞在析构函数中。
        猜你喜欢
        • 2011-04-03
        • 2010-12-16
        • 2017-06-22
        • 2012-04-10
        • 2012-11-14
        • 2016-09-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多