c++multithreadingthread-safetyiostream

Using std::ios_base::pword() in a thread-safe way?


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?


Solution

  • 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.