5

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

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

4

6 回答 6

3

通常,在您知道没有其他线程正在使用它之前,您不应该销毁一个对象。时期。

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

  • 线程 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_ptr使用or可以很容易地实现引用计数模型std::shared_ptrshared_ptr除非所有线程中的所有 s 都被销毁(或指向其他地方),否则析构函数将不会运行,因此在销毁的那一刻,您知道唯一指向剩余对象的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();
}

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

于 2011-07-15T16:29:03.490 回答
2

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

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

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

于 2011-07-15T16:28:36.543 回答
1

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

于 2011-07-15T16:29:39.620 回答
1

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

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

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

于 2011-07-15T16:29:41.890 回答
1

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

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

在调用析构函数之前对象的生命周期已经结束?

于 2014-03-02T18:46:59.067 回答
1

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

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

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

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

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

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

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

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

于 2017-01-12T17:55:11.480 回答