2
votes

I have the following class that is supposed to be thread safe and has a std::shared_ptr member that refers to some shared resource.

class ResourceHandle
{
    using Resource = /* unspecified */;
    std::shared_ptr<Resource> m_resource;
};

Multiple threads may acquire a copy of a resource handle from some central location and the central resource handle may be updated at any time. So reads and writes to the same ResourceHandle may take place concurrently.

// Centrally:
ResourceHandle rh;

// Thread 1: reads the central handle into a local copy for further processing
auto localRh = rh;

// Thread 2: creates a new resource and updates the central handle
rh = ResourceHandle{/* ... */};

Because these threads perform non-const operations on the same std::shared_ptr, according to CppReference, I should use the std::atomic_...<std::shared_ptr> specializations to manipulate the shared pointer.

If multiple threads of execution access the same shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.

So I want to implement the copy and move operations of the ResourceHandle class using these atomic operations such that manipulating a single resource handle from multiple threads avoids all data races.

The notes section of the CppReference page on std::atomic_...<std::shared_ptr> specializations states the following:

To avoid data races, once a shared pointer is passed to any of these functions, it cannot be accessed non-atomically. In particular, you cannot dereference such a shared_ptr without first atomically loading it into another shared_ptr object, and then dereferencing through the second object.

So I probably want to use some combination of std::atomic_load and std::atomic_store, but I am unsure where and when they should be applied.

How should the copy and move operations of my ResourceHandle class be implemented to not introduce any data races?

1
To be honest, I don't understand what you are asking here. Why should a widget be copyable at all? Where is the atomic that you don't use? Which class is "basically [..] the C++17 equivalent" of an atomic shared pointer? - Ulrich Eckhardt
shared_ptr already has atomic copy and move operations. - NathanOliver
@UlrichEckhardt Okay, Widget might indeed be a bad name, I'll change it. But the class is a kind of handle that provided access to a shared resource. Multiple threads may want a handle to the same resource and may acquire a copy from some central location. However, this handle may also be written to at any time. So, I want the copy/move operations to be thread safe. Does that make a bit more sense? If not, how could I make it more clear what I am asking? - Maarten Bamelis
Are you intending to move / swap a new Resource into the object owned by the m_resource, or to modify m_resource to own a different Resource? - Caleth
@UlrichEckhardt While it has fallen out of fashion, class Widget was the struct Foo of the 1990s and early 2000s. Scott Meyers uses the term extensively in his Items, iirc. - jonspaceharper

1 Answers

2
votes

std::shared_ptr synchronises it's access to the reference count, so you don't have to worry about operations on one std::shared_ptr affecting another. If those are followed by at least one modification to the pointee, you have a data race there. Code that shares ownership of a previous Resource will be unaffected by m_resource being reset to point to a new Resource.

You have to synchronise access to a single std::shared_ptr, if that is accessible in multiple threads. The warning provided (and the reason it is deprecated in C++20) states that if anywhere is atomically accessing a value, everywhere that accesses that value should be atomic.

You could achieve that by hiding the global std::shared_ptr behind a local copies. ResourceHandle as a separate class makes that more difficult.

using ResourceHandle = std::shared_ptr<Resource>;
static ResourceHandle global;

ResourceHandle getResource()
{
    return std::atomic_load(&global);
}

void setResource(ResourceHandle handle)
{
    std::atomic_store(&global, handle);
}