C++: Concurrency and destructors
Asked Answered
R

6

5

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?

Ringnecked answered 15/7, 2011 at 16:22 Comment(1)
Why are you destroying the object when there might still be threads that are accessing it?Burgee
C
3

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.

Cyrille answered 15/7, 2011 at 16:29 Comment(2)
Great answer! Always heard about shared_ptr, I'll start using it nowRingnecked
This answer has some good advice that can be used in some situations, but is in general wrong. "object destruction must be synchronized somewhere other than the object itself" – This is not true. A counterexample disproving the rule is std::condition_variable; clients are specifically allowed to invoke the destructor while other threads' calls to wait() are ongoing.Supply
C
2

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.

Concise answered 15/7, 2011 at 16:28 Comment(1)
This answer doesn't apply to all classes: I think in general you should treat most classes this way, but there are certainly some perfectly cromulent classes that support calling the destructor "before the use of the object ends."Supply
P
1

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.

Pudency answered 15/7, 2011 at 16:29 Comment(0)
J
1

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/.

Jecoa answered 15/7, 2011 at 16:29 Comment(0)
D
1

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?

Drakensberg answered 2/3, 2014 at 18:46 Comment(0)
S
1

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...

Supply answered 12/1, 2017 at 17:55 Comment(2)
Which criteria would you follow to decide whether to have the client sync destruction or sync it yourself?Ringnecked
@Ringnecked – The criteria I can think of are: simplicity for the client, e.g. additional states to think about, additional functions to forget etc; simplicity for the destructor, i.e. it is nice if it doesn't have to block; and I think if all else is equal you might as well do as the standard library does just for consistency, i.e. require the client to synchronize. Any others? So – if you can avoid blocking in the dtor without imposing any burden on the client, don't block in the destructor, I guess.Supply

© 2022 - 2024 — McMap. All rights reserved.