Search code examples
boostthread-safetylockingemplace

Why doesn't boost::lockfree::spsc_queue have emplace?


The regular std::vector has emplace_back which avoid an unnecessary copy. Is there a reason spsc_queue doesn't support this? Is it impossible to do emplace with lock-free queues for some reason?


Solution

  • I'm not a boost library implementer nor maintainer, so the rationale behind why not to include an emplace member function is beyond my knowledge, but it isn't too difficult to implement it yourself if you really need it.

    The spsc_queue has a base class of either compile_time_sized_ringbuffer or runtime_sized_ringbuffer depending on if the size of the queue is known at compilation or not. These two classes maintain the actual buffer used with the obvious differences between a dynamic buffer and compile-time buffer, but delegate, in this case, their push member functions to a common base class - ringbuffer_base.

    The ringbuffer_base::push function is relatively easy to grok:

    bool push(T const & t, T * buffer, size_t max_size)
    {
        const size_t write_index = write_index_.load(memory_order_relaxed);  // only written from push thread
        const size_t next = next_index(write_index, max_size);
    
        if (next == read_index_.load(memory_order_acquire))
            return false; /* ringbuffer is full */
    
        new (buffer + write_index) T(t); // copy-construct
    
        write_index_.store(next, memory_order_release);
    
        return true;
    }
    

    An index into the location where the next item should be stored is done with a relaxed load (which is safe since the intended use of this class is single producer for the push calls) and gets the appropriate next index, checks to make sure everything is in-bounds (with a load-acquire for appropriate synchronization with the thread that calls pop) , but the main statement we're interested in is:

    new (buffer + write_index) T(t); // copy-construct
    

    Which performs a placement new copy construction into the buffer. There's nothing inherently thread-unsafe about passing around some parameters to use to construct a T directly from viable constructor arguments. I wrote the following snippet and made the necessary changes throughout the derived classes to appropriately delegate the work up to the base class:

    template<typename ... Args>
    std::enable_if_t<std::is_constructible<T,Args...>::value,bool>
    emplace( T * buffer, size_t max_size,Args&&... args)
    {
        const size_t write_index = write_index_.load(memory_order_relaxed);  // only written from push thread
        const size_t next = next_index(write_index, max_size);
    
        if (next == read_index_.load(memory_order_acquire))
            return false; /* ringbuffer is full */
    
        new (buffer + write_index) T(std::forward<Args>(args)...); // emplace
    
        write_index_.store(next, memory_order_release);
    
        return true;
    }
    

    Perhaps the only difference is making sure that the arguments passed in Args... can actually be used to construct a T, and of course doing the emplacement via std::forward instead of a copy construction.