Home > Software design >  Woes with std::shared_ptr<T>.use_counter()
Woes with std::shared_ptr<T>.use_counter()

Time:03-29

https://en.cppreference.com/w/cpp/memory/shared_ptr/use_count states:

In multithreaded environment, the value returned by use_count is approximate (typical implementations use a memory_order_relaxed load)

But does this mean that use_count() is totally useless in a multi-threaded environment?

Consider the following example, where the Circular class implements a circular buffer of std::shared_ptr<int>.

One method is supplied to users - get(), which checks whether the reference count of the next element in the std::array<std::shared_ptr<int>> is greater than 1 (which we don't want, since it means that it's being held by a user which previously called get()).

If it's <= 1, a copy of the std::shared_ptr<int> is returned to the user.

In this case, the users are two threads which do nothing at all except love to call get() on the circular buffer - that's their purpose in life.

What happens in practice when I execute the program is that it runs for a few cycles (tested by adding a counter to the circular buffer class), after which it throws the exception, complaining that the reference counter for the next element is > 1.

Is this a result of the statement that the value returned by use_count() is approximate in a multi-threaded environment?

Is it possible to adjust the underlying mechanism to make it, uh, deterministic and behave as I would have liked it to behave?

If my thinking is correct - use_count() (or rather the real number of users) of the next element should never EVER increase above 1 when inside the get() function of Circular, since there are only two consumers, and every time a thread calls get(), it's already released its old (copied) std::shared_ptr<int> (which in turn means that the remaining std::shared_ptr<int> residing in Circular::ints_ should have a reference count of only 1).

#include <mutex>
#include <array>
#include <memory>
#include <exception>
#include <thread>

class Circular {
    public:
      Circular() {
          for (auto& i : ints_) { i = std::make_shared<int>(0); }
      }

      std::shared_ptr<int> get() {
        std::lock_guard<std::mutex> lock_guard(guard_);
        index_ = index_ % 2;  // Re-set the index pointer.

        if (ints_.at(index_).use_count() > 1) {
          // This shouldn't happen - right? (but it does)
          std::string excp = std::string("OOPSIE: ")   std::to_string(index_)   " "   std::to_string(ints_.at(index_).use_count());
          throw std::logic_error(excp);
        }

        return ints_.at(index_  );
      }

    private:
        std::mutex                          guard_;
        unsigned int                        index_{0};
        std::array<std::shared_ptr<int>, 2> ints_;
};

Circular circ;
void func() {
    do {
      auto scoped_shared_int_pointer{circ.get()};
    }while(1);
}

int main() {
  std::thread t1(func), t2(func);

  t1.join(); t2.join();
}

CodePudding user response:

While use_count is fraught with problems, the core issue right now is outside of that logic.

Assume thread t1 takes the shared_ptr at index 0, and then t2 runs its loop twice before t1 finishes its first loop iteration. t2 will obtain the shared_ptr at index 1, release it, and then attempt to acquire the shared_ptr at index 0, and will hit your failure condition, since t1 is just running behind.

Now, that said, in a broader context, it's not particularly safe, as if a user creates a weak_ptr, it's entirely possible for the use_count to go from 1 to 2 without passing through this function. In this simple example, it would work to have it loop through the index array until it finds the free shared pointer.

CodePudding user response:

use_count is for debugging only and shouldn't be used. If you want to know when nobody else has a reference to a pointer any more just let the shared pointer die and use a custom deleter to detect that and do whatever you need to do with the now unused pointer.

This is an example of how you might implement this in your code:

#include <mutex>
#include <array>
#include <memory>
#include <exception>
#include <thread>
#include <vector>
#include <iostream>

class Circular {
public:
  Circular() {
    size_t index = 0;
    for (auto& i : ints_)
    {
      i = 0;
      unused_.push_back(index  );
    }
  }

  std::shared_ptr<int> get() {
    std::lock_guard<std::mutex> lock_guard(guard_);

    if (unused_.empty())
    {
      throw std::logic_error("OOPSIE: none left");
    }
    size_t index = unused_.back();
    unused_.pop_back();
    return std::shared_ptr<int>(&ints_[index], [this, index](int*) {
      std::lock_guard<std::mutex> lock_guard(guard_);
      unused_.push_back(index);
    });
  }

private:
  std::mutex guard_;
  std::vector<size_t> unused_;
  std::array<int, 2> ints_;
};

Circular circ;
void func() {
  do {
    auto scoped_shared_int_pointer{ circ.get() };
  } while (1);
}

int main() {
  std::thread t1(func), t2(func);

  t1.join(); t2.join();
}

A list of unused indexes is kept, when the shared pointer is destroyed the custom deleter returns the index back to the list of unused indexes ready to be used in the next call to get.

  • Related