0

I am working on a small program that utilizes shared pointers. I have a simple class "Thing", that just is an class with an integer attribute:


class Thing{
public:
    Thing(int m){x=m;}
    int operator()(){
        return x;
    }
    void set_val(int v){
        x=v;
    }
    int x;
    ~Thing(){
        std::cout<<"Deleted thing with value "<<x<<std::endl;
    }

};

I have a simple function "fun", that takes in a shared_ptr instance, and an integer value, index, which just keeps track of which thread is outputting a given value. The function prints out the index value, passed to the function, and the reference count of the shared pointer that was passed in as an argument

std::mutex mtx1;
void fun(std::shared_ptr<Thing> t1,int index){
    std::lock_guard <std::mutex> loc(mtx1);
    int m=t1.use_count();
    std::cout<<index<<" : "<<m<<std::endl;
}

In main,I create one instance of a shared pointer which is a wrapper around a Thing object like so:

    std::shared_ptr<Thing> ptr5(nullptr);
    ptr5=std::make_shared<Thing>(110);

(declared this way for exception safety).

I then create 3 threads, each of which creates a thread executing the fun() function that takes in as arguments the ptr5 shared pointer and increasing index values:

    std::thread t1(fun,ptr5,1),t2(fun,ptr5,2),t3(fun,ptr5,3);
    t1.join();
    t2.join();
    t3.join();

My thought process here is that since each of the shared pointer control block member functions was thread safe, the call to use_count() within the fun() function is not an atomic instruction and therefore requires no locking. However, both running without and without a lock_guard,this still leads to a race condition. I expect to see an output of:

1:2 2:3 3:4

since each thread spawns a new reference to the original shared pointer, the use_count() will be incremented each time by 1 reference. However, my output is still random due to some race condition.

  • > the call to use_count() within the fun() function is not an atomic instruction and therefore requires no locking I think you are a bit confused about what atomics are, and what requires locking. – sbabbi Mar 17 '21 at 18:25
  • Could you expand on that a little? I'm still getting used to when to use locking and what is considered an atomic instruction. – Elliott Goldstein Mar 17 '21 at 18:43
  • 1
    Atomic instructions are those tied with a `std::atomic`. If you access a non atomic object concurrently from 2 threads, you need a lock (unless otherwise specified, for example `std::mutex::lock`). In your example, you access 3 *different* `shared_ptr` objects from 3 different threads. Furthermore, `shared_ptr::use_count` is const. So no lock is needed. A bit hard to expand in the comment section, but see for example: https://stackoverflow.com/q/14127379/666785 – sbabbi Mar 17 '21 at 18:52

1 Answers1

2

In a multithreaded environment, use_count() is approximate. From cppreference :

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

The control block for shared_ptr is otherwise thread-safe :

All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object.

Seeing an out-of-date use_count() is not an indication that the control-block was corrupted by a race condition.

Note that this does not extend to modifying the pointed object. It does not provide synchronization for the pointed object. Only the state of the shared_ptr and the control block are protected.

François Andrieux
  • 28,148
  • 6
  • 56
  • 87
  • I know that the object within the shared_ptr itself is not thread-safe, but what do you mean by approximate in this case? Would it mean there's no real way to synchronize this? – Elliott Goldstein Mar 17 '21 at 18:44
  • 1
    @ElliottGoldstein It means there is no useful way of using `use_count()` in a multithreaded context. – François Andrieux Mar 17 '21 at 18:48
  • @ElliottGoldstein by the time you have read the counter it may already have changed. – Richard Critten Mar 17 '21 at 18:50
  • Note that, in your test, there can be many more instances of `shared_ptr` which are stored within the `std::thread` and some, all or none of your test cases may have finished before the next thread starts, meaning there may be fewer simultaneous instances than you expect. – François Andrieux Mar 17 '21 at 18:50