I want to use pword()
to store an additional object with any stream, but in a thread-safe way, specifically to allocate exactly one instance of the object the first time around for a given stream. Since the void*
to which pword()
refers isn't atomic and therefore can't use a compare-exchange function, I believe I have to employ double-checked locking with memory barriers. Using this as a starting point, I have:
class my_obj { /* ... */ };
my_obj* get_obj_for( std::ios_base &b ) {
static int const index = ios_base::xalloc();
void *p = b.pword( index );
std::atomic_thread_fence( std::memory_order_acquire );
if ( p == nullptr ) {
static std::mutex mutex;
std::lock_guard<std::mutex> lock{ mutex };
p = b.pword( index );
if ( p == nullptr ) {
p = new my_obj;
std::atomic_thread_fence( std::memory_order_release );
b.pword( index ) = p;
}
}
return static_cast<my_obj*>( p );
}
However, in the "Using C++11 Acquire and Release Fences" example given by that article, its pointer m_instance
is std::atomic
. Will my above code still be thread-safe even though the void*
to which pword()
refers is not std::atomic
? If not, is there any way to make the code thread-safe?
Your right, the non-atomic read of the pointer at void *p = b.pword( index );
would not be thread-safe with respect to the assignment at b.pword( index ) = p;
.
You could try to fix this by using a std::atomic_ref<void*>(b.pword(index));
, but you run into the problem that pword()
itself isn't thread-safe.
Also, the result of pword()
can be invalidated by any operation in the stream, so b.pword(index) = p
can be invalid if any operation on the stream is called in any other thread concurrently.
You have to lock around pword()
. And you have to use the same lock for all operations on the stream.
It might be easier to just use a map:
my_obj* get_obj_for( std::ios_base &b ) {
static std::unordered_map<std::ios_base*, my_obj> map;
static std::shared_mutex m;
{
std::shared_lock _(m);
auto it = map.find(&b);
if (it != map.end()) return &it->second;
}
{
std::unique_lock _(m);
auto [ it, inserted ] = map.try_emplace(&b);
if (inserted) {
b.register_callback([](ios_base::event e, ios_base& b, int) {
if (e != ios_base::event::erase_event) return;
map.erase(&b);
}, 0);
}
return &it->second;
}
}
Also consider a redesign of what you want to do. You can instead wrap a stream in another stream that contains the my_obj
, and use the wrapped stream instead, like std::osyncstream
.