6
std::shared_ptr<Dog> pd;

void F() {
    pd = std::make_shared<Dog>("Smokey");
}

int main() {
    std::thread t1(F);
    std::thread t2(F);
    t1.join();
    t2.join();
    return 0;
}
std::shared_ptr<Dog> pd(new Dog("Gunner"));

void F() {
    std::shared_ptr<Dog> localCopy = pd;
}

int main() {
    std::thread t1(F);
    std::thread t2(F);
    t1.join();
    t2.join();
    return 0;
}

In C++, I understand that std::shared_ptr is thread-safe for reading and copying. But I'm a little confused about when I need to use a mutex to synchronize threads. I have two code snippets. In the first one, std::shared_ptr is being modified by multiple threads. In the second one, each thread is only reading from and copying the shared pointer. Do I need a mutex in both situations, or just in the first one? Why or why not?"

2
  • 1
    Uh-oh, you're getting perilously close to asking the worst question in all of C++ :) youtu.be/lkgszkPnV8g?t=1213 Commented Jun 30, 2023 at 1:29
  • 1
    TLDR: No. Astd::shared_ptr variable is no more thread safe than any other type of variable. If more than one thread accesses (reads or writes) the variable, and if at least one of them writes the variable, then all of those accesses must be "synchronized." One way to synchronize is for all of them to have the same mutex locked whenever they access the variable. Commented Jun 30, 2023 at 2:54

2 Answers 2

10

It's easier to understand this if you carefully identify all the moving pieces involved:

  • a reference-counted object, this is an object, somewhere, this is your Dog

  • the reference counter itself, this keeps track of the number of references to the reference-counted object

  • the shared pointer itself, that uses the reference counter

These are all, discrete entities, that need to be considered, and evaluated, separately.

As a rule of thumb, in C++ if an object is accessed from multiple execution threads, and at least one execution thread "modifies" it, in some way, then the execution threads must be "synchronized" with respect to this object; unless the object is "thread-safe". What does "synchronize" mean? Well, it means more than just a mutex, somewhere; but for this practical example this is what it means: you need to access the object while holding a lock on some mutex, somewhere.

Data point: the reference counter is thread-safe. The smart pointer, a.k.a std::shared_ptr is not.

pd = std::make_shared<Dog>("Smokey");

This modifies the shared pointer, pd, in multiple execution threads. This requires synchronization, this is not thread safe. You need a mutex.

std::shared_ptr<Dog> localCopy = pd;

This makes a copy of pd, it does not modify it. This also juggles the reference counter, as part of making a copy (and destroying) it. The reference-counter is thread safe. The shared pointer is not being modified, only accessed. This is thread safe.

Sign up to request clarification or add additional context in comments.

6 Comments

You don't need a std::mutex for interacting with std::shared_ptr at all. There's std::atomic<std::shared_ptr>. The mutex only becomes necessary once you concurrently modify Dog.
@JanSchultke, In your own answer, you admit that std::atomic<std::shared_ptr> is only available since C++20. There might be some projects out there that have not yet upgraded from an older C++ version.
@SolomonSlow unless you're using C++98 (in which case there isn't even a std::shared_ptr), you have the standalone atomic functions available besides the specialization. There's really no good reason to use std::mutex then.
@JanSchultke, IMO, there's no good reason to ever encourage a junior dev to use "atomic" anything. You can use a mutex to solve most of the same problems that you can solve with atomics, and you can use a mutex to solve a whole lot of other problems that you can't solve with atomics. The only places where atomics are truly needed are in code that junior devs should not be allowed to touch. Understanding how atomics behave is as simple as dirt, but understanding how to use that behavior effectively requires a whole new way of thinking. I say, "Let the newbies become masters of the mutex first!"
@SolomonSlow for what it's worth, I don't think junior devs should be writing critical multi-threading code at all. Multi-threading is one of the hardest things to get right, and using libraries like OMP that don't require you to touch threading primitives for simple things should be preferred. And if devs are writing "low-level" multi-threaded code, they may as well use the best tool available.
|
1

A std::shared_ptr<Dog> consists of three components:

  1. the std::shared_ptr smart pointer itself
  2. the pointed-to atomic reference counter
  3. the pointed-to Dog object

The form of synchronization you need depends on which of these you want to concurrently modify.

1 Concurrently Modifying the Smart Pointer

// for example
pd = std::make_shared<Dog>("Smokey");

This is not thread-safe, because multiple threads are concurrently modifying the same std::shared_ptr. There are basically three options:

Ideally, use on of the tools specially made for std::shared_ptr. std::mutex will be less efficient by comparison, and is also less convenient to use.

2 Concurrently Modifying the Reference Counter

// for example
std::shared_ptr<Dog> localCopy = pd;

You don't need std::mutex or anything else here, because it is already thread-safe. Copying a std::shared_ptr won't modify the original object, and it will thread-safely update the (atomic) reference counter.

3 Concurrently Modifying the Pointed-To Object

// for example
localCopy->woof();

If your Dog's woof member functions isn't thread safe, you will need to use some synchronization to ensure thread-safety. std::mutex is a good candidate for that, but you could also consider using std::atomic.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.