C++:并发和析构函数

发布于 2024-11-24 20:03:29 字数 157 浏览 4 评论 0原文

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

我的思路:假设我进入析构函数,我必须等待临界区,因为其他线程仍在使用它。一旦他完成了,我就可以完成对该物体的销毁。这有道理吗?

Suppose you have an object which can be accesed by many threads. A critical section is used to protect the sensitive areas. But what about the destructor? Even if I enter a critical section as soon as I enter the destructor, once the destructor has been called, is the object already invalidated?

My train of thought: Say I enter the destructor, and I have to wait on the critical section because some other thread is still using it. Once he is done, I can finish destroying the object. Does this make sense?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

蓝眼睛不忧郁 2024-12-01 20:03:29

一般来说,在知道没有其他线程正在使用某个对象之前,不应销毁该对象。时期。

根据您的“思路”考虑以下场景:

  • 线程 A:获取对象 X 引用
  • 线程 A:锁定对象 X
  • 线程 B:获取对象 X 引用
  • 线程 B:阻止对象 X 锁定
  • 线程 A:解锁对象 X
  • 线程 B:锁定对象X;解锁对象X; destroy object X

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

  • 线程 A:获取对象 X 引用
  • 线程 B:获取对象 X 引用
  • 线程 B:锁定对象 X;解锁对象X; destroy object X
  • 线程 A: Lock object X - crash

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

使用 boost 可以轻松实现引用计数模型: :shared_ptrstd::shared_ptr;除非所有线程中的所有 shared_ptr 都已被销毁(或指向其他地方),否则析构函数不会运行,因此在销毁时您知道指向剩余对象的唯一指针是 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 上同时读取是安全的,因此如果对您的应用程序有意义,您可以选择使用读写锁。

In general, you should not destroy an object until you know that no other thread is using it. Period.

Consider this scenario, based on your 'train of thought':

  • Thread A: Get object X reference
  • Thread A: Lock object X
  • Thread B: Get object X reference
  • Thread B: Block on object X lock
  • Thread A: Unlock object X
  • Thread B: Lock object X; unlock object X; destroy object X

Now consider what happens if the timing is slightly different:

  • Thread A: Get object X reference
  • Thread B: Get object X reference
  • Thread B: Lock object X; unlock object X; destroy object X
  • Thread A: Lock object X - crash

In short, object destruction must be synchronized somewhere other than the object itself. One common option is to use reference counting. Thread A will take a lock on the object reference itself, preventing the reference from being removed and the object being destroyed, until it manages to increment the reference count (keeping the object alive). Then thread B merely clears the reference and decrements the reference count. You can't predict which thread will actually call the destructor, but it will be safe either way.

The reference counting model can be implemented easily by using boost::shared_ptr or std::shared_ptr; the destructor will not run unless all shared_ptrs in all threads have been destroyed (or made to point elsewhere), so at the moment of destruction you know that the only pointer to the object remaining is the this pointer of the destructor itself.

Note that when using shared_ptr, it's important to prevent the original object reference from changing until you can capture a copy of it. Eg:

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();
}

Note that simultaneous reads on a shared_ptr is safe, so you can choose to use a read-write lock if it makes sense for your application.

剧终人散尽 2024-12-01 20:03:29

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

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

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

If a object is in use then you should make sure that the destructor of the object is not being called before the use of the object ends. If this is the behavior you have then its a potential problem and it really needs to be fixed.

You should make sure that if one thread is destroying your objects then another thread should not be calling functions on that object or the first thread should wait till second thread completes the function calling.

Yes, even destructors might need critical sections to protect updating some global data which is not related to the class itself.

廻憶裏菂餘溫 2024-12-01 20:03:29

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

It's possible that while one thread is waiting for CS in destructor the other is destroying the object and if CS belongs to object it will be destroyed as well. So that's not a good design.

脸赞 2024-12-01 20:03:29

您绝对需要确保对象的生命周期短于消费者线程,否则您将遇到一些严重的麻烦。要么:

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

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

You absolutely, positively need to make sure your object lifetime is less than the consumer threads, or you are in for some serious headaches. Either:

  1. Make the consumers of the object children so it's impossible for them to exist outside of your object, or
  2. use message passing/broker.

If you go the latter route, I highly recommend 0mq http://www.zeromq.org/.

贱人配狗天长地久 2024-12-01 20:03:29

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

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

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

Yes while you are in destructor, the object is already invalidated.

I used Destroy() method that enters critical section and then destroys it self.

Lifetime of object is over before destructor is called?

小姐丶请自重 2024-12-01 20:03:29

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

我建议客户不要假设他们可以做到这一点,除非有明确的记录。默认情况下,客户端确实有这种负担,尤其是标准库对象(§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),直到所有请求都完成。有一些权衡...

Yes, it is fine to do that. If a class supports such use, clients don't need to synchronize destruction; i.e. they don't need to make sure that all other methods on the object have finished before invoking the destructor.

I would recommend that clients not assume they can do this unless it is explicitly documented. Clients do have this burden, by default, with standard library objects in particular(§17.6.4.10/2).

There are cases where it is fine, though; std::condition_variable's destructor, for example, specifically allows ongoing condition_variable::wait() method invocations when ~condition_variable() starts. It only requires that clients not initiate calls to wait() after ~condition_variable() starts.

It might be cleaner to require that the client synchronize access to the destructor – and constructor for that matter – like most of the rest of the standard library does. I would recommend doing that if feasible.

However, there are certain patterns where it might make sense to relieve clients of the burden of fully synchronizing destruction. condition_variable's overall pattern seems like one: consider use of an object that handles possibly long-running requests. The user does the following:

  1. Construct the object
  2. Cause the object to receive requests from other threads.
  3. Cause the object to stop receiving requests: at this point, some outstanding requests might be ongoing, but no new ones can be invoked.
  4. Destroy the object. The destructor will block until all requests are done, otherwise the ongoing requests might have a bad time.

An alternative would be to require that clients do need to synchronize access. You could imagine step 3.5 above where the client calls a shutdown() method on the object that does the blocking, after which it is safe for the client to destroy the object. However, this design has some downsides; it complicates the API and introduces an additional state for the object of shutdown-but-valid.

Consider instead perhaps getting step (3) to block until all requests are done. There are tradeoffs...

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文