C++11 —— 单生产者/单消费者 的 FIFO 无锁队列

  发现 zeromq 的 yqueue_t 模板类,其数据存储理念设计得非常妙。借这一理念,按照 STL 的泛型类 queue 的接口标准,我设计了一个线程安全的 单生产者/单消费者(单线程push/单线程pop) FIFO 队列,以此满足更为广泛的应用。

1. 数据存储理念的结构图

type_index

  • 队列的整体结构上,使用链表的方式,将多个固定长度的 chunk 串联起来;
  • 每个 chunk 则可用于存储队列所需要的元素;
  • 增加一个可交换的 chunk 单元,利于内存复用;
  • 队列使用时,支持 单个线程的 push(生产) 和 单个线程 pop(消费)的并发操作(内部并未加锁)。

2. 源码 (xqueue.h)

/**
 * @file xqueue.h
 * Copyright (c) 2021 Gaaagaa. All rights reserved.
 * 
 * @author  : Gaaagaa
 * @date    : 2019-11-29
 * @version : 1.0.0.0
 * @brief   : 实现双线程安全的 单生产者/单消费者 FIFO 队列。
 */

/**
 * The MIT License (MIT)
 * Copyright (c) 2019, Gaaagaa All rights reserved.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#ifndef __XQUEUE_H__
#define __XQUEUE_H__

#include <memory>
#include <atomic>
#include <cassert>

////////////////////////////////////////////////////////////////////////////////
// xqueue_t : single producer/single consumer FIFO queue

/**
 * @class xqueue_t
 * @brief 双线程安全的 单生产者/单消费者 FIFO队列。
 * 
 * @tparam __elem_t  : 队列存储的节点元素类型。
 * @tparam __csize_v : 队列中的存储分块可容纳节点元素的数量。
 * @tparam __alloc_t : 元素对象分配器。
 */
template< typename __elem_t,
          size_t   __csize_v = 16,
          typename __alloc_t = std::allocator< __elem_t > >
class xqueue_t : protected __alloc_t
{
    static_assert(__csize_v >= 4,
                  "__csize_v size value must be greater than or equal to 4!");

    // common data types
public:
    using value_type      = __elem_t;
    using reference       = __elem_t &;
    using const_reference = const __elem_t &;
    using size_type       = size_t;

    static constexpr const size_type xchunk_size = __csize_v;

private:
    /** 前置声明 存储分块 的类型 */
    struct x_chunk_t;

    using x_chkptr_t  = struct x_chunk_t *;
    using x_chkpos_t  = size_t;
    using x_array_t   = value_type[xchunk_size];
    using x_swapchk_t = std::atomic< x_chkptr_t >;
    using x_quesize_t = std::atomic< size_type >;

    using x_alvalue_t = typename std::allocator_traits<
                            __alloc_t >::template rebind_alloc< __elem_t >;
    using x_alchunk_t = typename std::allocator_traits<
                            __alloc_t >::template rebind_alloc< x_chunk_t >;

    /**
     * @struct x_chunk_t
     * @brief  存储元素节点的连续内存块结构体。
     */
    typedef struct x_chunk_t
    {
        x_chkptr_t xchk_next;  ///< 指向后一内存块节点
        x_array_t  xchk_elem;  ///< 当前内存块中的元素节点数组
    } x_chunk_t;

    // constructor/destructor
public:
    explicit xqueue_t(void)
        : m_chk_swap(nullptr)
        , m_que_size(0)
        , m_que_front({ nullptr, 0 })
        , m_que_back ({ nullptr, 0 })
    {
        m_que_front.m_chk_vptr = alloc_chunk();
        m_que_back.m_chk_vptr  = m_que_front.m_chk_vptr;
    }

    ~xqueue_t(void)
    {
        while (size() > 0)
            pop();

        assert(m_que_front.m_chk_vptr == m_que_back.m_chk_vptr);
        recyc_chunk(m_que_front.m_chk_vptr);
        recyc_chunk(nullptr);
    }

    xqueue_t(xqueue_t && xobject) = delete;
    xqueue_t & operator = (xqueue_t && xobject) = delete;
    xqueue_t(const xqueue_t & xobject) = delete;
    xqueue_t & operator = (const xqueue_t & xobject) = delete;

    // public interfaces
public:
    /**********************************************************/
    /**
     * @brief 当前队列中的元素数量。
     */
    inline size_type size(void) const
    {
        return m_que_size;
    }

    /**********************************************************/
    /**
     * @brief 判断队列是否为空。
     */
    inline bool empty(void) const
    {
        return (0 == size());
    }

    /**********************************************************/
    /**
     * @brief 向队列后端压入一个元素。
     */
    void push(const value_type & xelem_value)
    {
        if (size() > 0)
        {
            back_forward();
        }

        x_alvalue_t::construct(
            &m_que_back.m_chk_vptr->xchk_elem[m_que_back.m_chk_npos],
            xelem_value);
        m_que_size.fetch_add(1);
    }

    /**********************************************************/
    /**
     * @brief 以右值引用方式,向队列后端压入一个元素。
     */
    void push(value_type && xelem_value)
    {
        if (size() > 0)
        {
            back_forward();
        }

        x_alvalue_t::construct(
            &m_que_back.m_chk_vptr->xchk_elem[m_que_back.m_chk_npos],
            std::forward< value_type >(xelem_value));
        m_que_size.fetch_add(1);
    }

    /**********************************************************/
    /**
     * @brief 向队列后端压入一个元素。
     */
    template< typename... __args_t >
    decltype(auto) emplace(__args_t &&... xargs)
    {
        if (size() > 0)
        {
            back_forward();
        }

        x_alvalue_t::construct(
            &m_que_back.m_chk_vptr->xchk_elem[m_que_back.m_chk_npos],
            std::forward< __args_t >(xargs)...);
        m_que_size.fetch_add(1);

        return back();
    }

    /**********************************************************/
    /**
     * @brief 从队列前端弹出一个元素。
     */
    void pop(void)
    {
        assert(size() > 0);

        x_alvalue_t::destroy(
            &m_que_front.m_chk_vptr->xchk_elem[m_que_front.m_chk_npos]);
        if (m_que_size.fetch_sub(1) > 1)
        {
            front_forward();
        }
    }

    /**********************************************************/
    /**
     * @brief 返回队列前端元素。
     */
    inline reference front(void)
    {
        assert(!empty());
        return m_que_front.m_chk_vptr->xchk_elem[m_que_front.m_chk_npos];
    }

    /**********************************************************/
    /**
     * @brief 返回队列前端元素。
     */
    inline const_reference front(void) const
    {
        assert(!empty());
        return m_que_front.m_chk_vptr->xchk_elem[m_que_front.m_chk_npos];
    }

    /**********************************************************/
    /**
     * @brief 返回队列后端元素。
     */
    inline reference back(void)
    {
        assert(!empty());
        return m_que_back.m_chk_vptr->xchk_elem[m_que_back.m_chk_npos];
    }

    /**********************************************************/
    /**
     * @brief 返回队列后端元素。
     */
    inline const_reference back(void) const
    {
        assert(!empty());
        return m_que_back.m_chk_vptr->xchk_elem[m_que_back.m_chk_npos];
    }

    // internal invoking
private:
    /**********************************************************/
    /**
     * @brief 申请分块。
     */
    inline x_chkptr_t alloc_chunk(void)
    {
        x_chkptr_t xchunk_ptr = m_chk_swap.exchange(nullptr);
        if (nullptr == xchunk_ptr)
        {
            x_alchunk_t xalloc_chunk;
            xchunk_ptr = xalloc_chunk.allocate(1);
            assert(nullptr != xchunk_ptr);
        }

        xchunk_ptr->xchk_next = nullptr;
        return xchunk_ptr;
    }

    /**********************************************************/
    /**
     * @brief 回收分块。
     */
    inline void recyc_chunk(x_chkptr_t xchunk_ptr)
    {
        x_chkptr_t xchk_rptr = m_chk_swap.exchange(xchunk_ptr);
        if (nullptr != xchk_rptr)
        {
            x_alchunk_t xalloc_chunk;
            xalloc_chunk.deallocate(xchk_rptr, 1);
        }
    }

    /**********************************************************/
    /**
     * @brief 将前端位置向后移(该接口仅由 pop() 接口调用)。
     */
    inline void front_forward(void)
    {
        if (++m_que_front.m_chk_npos == xchunk_size)
        {
            assert(nullptr != m_que_front.m_chk_vptr);
            assert(nullptr != m_que_front.m_chk_vptr->xchk_next);

            x_chkptr_t xchunk_ptr = m_que_front.m_chk_vptr;
            m_que_front.m_chk_vptr = xchunk_ptr->xchk_next;
            m_que_front.m_chk_npos = 0;

            recyc_chunk(xchunk_ptr);
        }
    }

    /**********************************************************/
    /**
     * @brief 将后端位置向后移(该接口仅由 push() 接口调用)。
     */
    inline void back_forward(void)
    {
        if (++m_que_back.m_chk_npos == xchunk_size)
        {
            assert(nullptr != m_que_back.m_chk_vptr);
            assert(nullptr == m_que_back.m_chk_vptr->xchk_next);

            x_chkptr_t xchunk_ptr = alloc_chunk();
            m_que_back.m_chk_vptr->xchk_next = xchunk_ptr;

            m_que_back.m_chk_vptr = xchunk_ptr;
            m_que_back.m_chk_npos = 0;
        }
    }

    // data members
protected:
    x_swapchk_t     m_chk_swap;   ///< 用于保存交换内存块(备用缓存块)
    x_quesize_t     m_que_size;   ///< 队列中的有效对象数量

    /** 指向队列前端的分块存储信息 */
    struct
    {
        x_chkptr_t  m_chk_vptr;   ///< 存储分块
        x_chkpos_t  m_chk_npos;   ///< 当前存储节点的索引号
    } m_que_front;

    /** 指向队列后端的分块存储信息 */
    struct
    {
        x_chkptr_t  m_chk_vptr;   ///< 存储分块
        x_chkpos_t  m_chk_npos;   ///< 当前存储节点的索引号
    } m_que_back;
};

////////////////////////////////////////////////////////////////////////////////

#endif // __XQUEUE_H__

3. 使用示例

#include "xqueue.h"
#include <iostream>
#include <thread>
#include <chrono>

////////////////////////////////////////////////////////////////////////////////

int main(int argc, char * argv[])
{
    using x_int_queue_t = xqueue_t< int, 8 >;

    x_int_queue_t spsc;

    std::cout << "sizeof(x_int_queue_t) : " << sizeof(x_int_queue_t) << std::endl;

    bool b_push_finished = false;
    std::thread xthread_in([&spsc, &b_push_finished](void) -> void
    {
        for (int i = 0; i < 1000; ++i)
        {
            spsc.push(i);
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }

        b_push_finished = true;
    });

    std::thread xthread_out([&spsc, &b_push_finished](void) -> void
    {
        int i = 0;
        while (true)
        {
            if (!spsc.empty())
            {
                std::cout << "[" << ++i   << "] "
                          << spsc.size()  << ", "
                          << spsc.front() << std::endl;
                spsc.pop();
                std::this_thread::sleep_for(std::chrono::milliseconds(1));
            }
            else if (b_push_finished)
            {
                break;
            }
        }
    });

    if (xthread_in.joinable())
    {
        xthread_in.join();
    }

    if (xthread_out.joinable())
    {
        xthread_out.join();
    }

    return 0;
}

////////////////////////////////////////////////////////////////////////////////

原文链接: https://www.cnblogs.com/Gaaagaa/p/12130787.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    C++11 —— 单生产者/单消费者 的 FIFO 无锁队列

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/191274

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 下午5:41
下一篇 2023年2月12日 下午5:41

相关推荐