0

I found an example here , about using an observer thread with an weak pointer:

std::thread observer;

void observe(std::weak_ptr<int> wp) {
 //Start observer thread
 observer = std::thread([wp](){
  while(true) {
   std::this_thread::sleep_for(std::chrono::seconds(1));

   //Try acquiring a shared_ptr from weak_ptr
   if(std::shared_ptr<int> p = wp.lock()) {
    //Success
    std::cout << "Observing: " << *p << "\n";
   } else {
    //The managed object is destroyed. 
    std::cout << "Stop\n";
    break;
   }
  }
 });
}

But I was wondering why use a weak pointer?

The observer function could very much look like this, using a copy of the shared pointer through the function parameter:

void observe(std::shared_ptr<int> sp) {
 observer = std::thread([sp](){
  while(true) {
   std::this_thread::sleep_for(std::chrono::seconds(1));

   if(sp.use_count() > 1) {
    //Success
    std::cout << "Observing: " << *p << "\n";
   } else {
    //The managed object is about to be destroyed. 
    std::cout << "Stop\n";
    break;
   }
  }
 });
}

So if the counter for the pointed object is 1 we know for sure that the dinamic object will be deleted when the function returns.

Useless
  • 64,155
  • 6
  • 88
  • 132
  • 3
    I'd rather see both the _before_ and _after_ code snippets presented in the question (as opposed to a link to the original). – Wyck Aug 18 '22 at 13:51
  • We don't know for sure the object will be destroyed when the function returns. There could be a weak-pointer in another thread that locks the object while that function is executing. Trying to apportion meaning to use counts (outside the implementation that when it reaches 0 you delete the object, is the road to hell. – Persixty Aug 18 '22 at 15:30
  • 1
    `But I was wondering why use a weak pointer?` To prevent strong reference cycle which will lead to memory leak. https://youtu.be/JfmTagWcqoE – Marek R Aug 18 '22 at 17:33
  • 1
    For all the reasons it's a bad idea how about waiting for `use_count()` to reach 1 is a one trick pony. Two observer threads would be in a 'stand-off' both waiting for the other to release the object so they become the last owner! – Persixty Aug 18 '22 at 17:37

3 Answers3

2

There's a useful and interesting discussion in this question about possible data races introduced by shared_ptr::use_count, which was originally intended solely for use in debugging.

But leaving all that aside, consider what happens in your example when a second observer is independently added. Now, the use count never drops below 2 and the shared_ptr is never deleted. But the weak reference solution would continue to work, with no need for deep thought about potential races.

Perhaps in the initial design, a second observer was never contemplated. But that doesn't mean that it won't seem like a good idea at some point in the future

rici
  • 234,347
  • 28
  • 237
  • 341
2

In the provided first example the 'observer' polls (approx.) every second to see if the observed object has expired. That's because wp.lock() returns a shared-pointer to the object iff it has not expired (wp.expired()==false).

The comment //The managed object is destroyed. is not completely accurate. The object has expired and has been destroyed or some thread is about to or is in the process of destroying it - but it may not be destroyed.

So that slight inaccuracy aside the code works.

However the code proposed in the second example assumes that because the use_count() is 1 the std::shared_ptr<int> sp is "the last of it's kind" and when that shared-owner is destructed on return of the lambda-function then the object shall be destroyed.

Not so in general. There may be a weak_ptr<> referring to that object and if a call to lock() on that object happens after the call to use_count()(*) the use count will be increased to 2 and returning from the lambda-function (as the thread enters termination) will not cause the object to be destroyed.

The point here is that as soon as any weak_ptr<> returns true from expired() the object is in train to be destroyed and no valid use can obtain another shared_ptr<> to it. But shared-pointers can be obtained from weak-pointers (if at least one shared-pointer remains).

See this note from C++ Reference on expired() which is quite telling (my emphasis):

This function is inherently racy if the managed object is shared among threads. In particular, a false result may become stale before it can be used. A true result is reliable.

But while there is a shared_ptr<> any weak_ptr<>::lock() may succeed in a "lock" on the object and create a new shared_ptr<> but once the object is expired, it shall not / cannot be (somehow) reprieved.

This is a common feature of 'reference counting' designs. The only truly reliable count is 0 - there's "no coming back from that". If you don't implement weak-pointers you can with effort count references and work out if you can account for all the sharing owners. But if you do implement weak-pointers that game is up.

If you then decide to start counting weak-pointers and making decisions about destruction based on them, well they're not weak-pointers anymore and that's a different game all together.

However do notice that reasoning about 'reference count' is a one trick pony. It only takes two observer threads (from the Question) to be in a deadlock where each is waiting for the other to release their ownership to destroy the object. Some corner of code can play that game but it's not a reliable model for managing shared ownership.

Footnote:

I strongly advise against using use_count() for anything other than debugging because the only really meaningful return value is 0 and that is equivalent to expired()==true anyway.

IMHO use_count() is an uncommon error in the C++ Standard Library in which 'helper debug' members have been exposed and inevitably lead developers astray as well as nailing the class to a particular design which the Standard Library generally does not.

(*) Or is otherwise not reflected in the use count returned such as relaxed memory barriers regarding the count returned.

Persixty
  • 8,165
  • 2
  • 13
  • 35
1

Besides the sharp edges of use_count that make it much more difficult to use correctly than weak_ptr, your program does not behave the same as the initial example.

In your code the shared_ptr captured by the thread will keep the object alive as long as the observing thread is running. That means that in the worst case you will have to wait around 1 second after the last owner died before the observed object gets actually destroyed. Contrast that with the initial example where you would at most have to wait for the weak_ptr to release its copy, which is both much less likely to occur and has a much shorter expected effect on the lifetime of the object.

In case of an int, waiting one second for destruction might not seem like a big deal. But consider instead a more pathological example where the observed object executes a substantial operation in its destructor and the observer thread runs with a frequency of a single check per day...

ComicSansMS
  • 51,484
  • 14
  • 155
  • 166
  • 1
    Another issue is if the deleter held by the shared pointer has thread-affinity, e.g. in many gui libraries only the gui thread can delete widgets – Caleth Aug 18 '22 at 14:21