zmq无锁队列的学习

前言

这里主要对之前学习的一些服务器组件和框架进行一些总结和复习,最近开始找秋招了,就通过写一些CSDN对之前的技术进行总结吧

为什么要使用无锁队列

C/C++服务器编程的两个重要方向就是并行与异步化,于是我在学习服务器的时候便在一些博客上见到了无锁队列的优化,无锁队列顾名思义其实就是无锁的队列,既然是队列为什么又要分有锁和无锁了,这里我先根据我的理解解释一下(只是大三学生,能力有限,并不是大佬,只想写博客记录一下自己的学过的技术点,有错误请指出,谢谢!)。

为什么需要锁

其实对于锁的提出大家并不陌生,锁与多线程编程是息息相关的,多线程编程也是服务器并行优化的一个非常重要的方面,既然使用了多线程,就不得不考虑临界资源管理的问题,常规的我们会采用 mutex+condition+notify进行处理,我们会利用加锁和条件变量机制来管理临界资源,利用notify来通知相应的资源情况,我们知道这些机制会为系统带来等待唤醒开销、竞争开销、条件变量开销等一系列开销,所以我们提出了无锁队列的队列结构。

无锁队列真的无锁吗

无锁队列知识避开了mutex的加锁机制,利用cas 原子操作来替代了加锁解锁的操作,对于什么是CAS(compare and swap)原子操作网上有很多技术大佬的博客,后续会更新相关博客,那么这个时候我又思考了另一个问题,原子操作真的就比加锁快吗,我在思考这个问题是因为其实无锁队列在进行CAS原子操作的时候,同样涉及一个问题,CAS操作会在用户态下不断的循环检测,这个过程对CPU消耗也是不小的。

带着疑问,结合网上zmq队列的开源代码我进行了一些测试。

zmq无锁队列的整体设计

zmq无锁队列的数据结构

对象数据成员

class yqueue_t {
private:
    struct chunk_t {
        T values[N];
        chunk_t *prev;
        chunk_t *next;
    };

    chunk_t *begin_chunk; // 链表头结点
    int beign_pos; // 起始点
    chunk_t *end_chunk; // 拿来扩容的,总是指向链表的最后一个结点
    int end_pos; 
    chunk_t *back_chunk; // 队列中最后一个元素所在的链表结点
    int back_pos; // 尾部

    atomic_ptr_t<chunk_t> spare_chunk;
    // 私有化拷贝构造函数
    yqueue_t(const yqueue_t &);
    // 私有化赋值构造函数
    const yqueue_t &operator=(const yqueue_t &);
};

其数据成员类似于vector中的三个封装迭代器begin、back、end,这里的chunk_t的数结构其实是对队列空间的申请分配进行了一个优化,队列的基本单元是一个chunk_t而一个chunk_t的数据结构里面有封装了N个真正的数据,这样我们在为队列开辟空间的时候不至于频繁开辟空间,节省了空间开辟申请的时间。

方法

其实对于方法的说明和队列大同小异都是push,pop加上构造函数与析构函数这里采用了inline内联的方式,对于较为复杂的函数方法不建议采用内联函数的方式,同时这里将构造函数设置为 inline,需要注意,如果在以后设计构造函数时如果体现了动态多态还想使用 inline的话,需要再子类的构造函数初始化列表中加上父类的构造函数,否在体现动态多态时会出现基类无法构造的问题。

对于无锁队列的各个操作函数处理看源代码注释已经很好理解了,这里不做过多说明。直接上代码和开源的代码一样。

// 创建队列
    inline yqueue_t() {
        // 为chunk分配内存空间
        begin_chunk = (chunk_t*) malloc(sizeof(chunk));
        alloc_assert(begin_chunk);
        begin_pos = 0;
        back_chunk = NULL;
        back_pos = 0;
        end_chunk = begin_chunk;
        end_pos = 0;
    }
    // 销毁队列
    inline ~yqueue_t() {
        while(true) {
            if(begin_chunk == end_chunk) { // 如果队列为空
                free(beign_chunk);
                break;
            }
            // 释放当前chunk_t
            chunk_t *o = begin_chunk;
            begin_chunk = begin_chunk->next;
            free(o);
        }
        // 将原子变量指针设置为NULL
        chunk_t *sc = spare_chunk.xchg(NULL);
        free(sc);
    }
    inline T &front() {
        return begin_chunk->values[begin_pos];
    }
    inline T &back() {
        return back_chunk.values[back_pos];
    }
    inline void push() {
        back_chunk = end_chunk;
        back_pos = end_pos;

        if(++end_pos != N) { // 当前chunk结点未满
            return ;
        }
        // 设置NULL,如果把之前的值取出来了则没有spare_chunk了
        chunk_t *sc = spare_chunk.xchg(NULL);
        if(sc) {
            end_chunk->next = sc;
            sc->prev = end_chunk;
        }
        else { // 没有则重新分配
            end_chunk->next = (chunk_t *) malloc(sizeof(chunk_t));
            alloc_assert(end_chunk->next);
            end_chunk->next->prev = end_chunk;
        }
        end_chunk = end_chunk->next;
        end_pos = 0;
    }

    // 删除队列尾部的元素
    inline void unpush() {
        if(back_pos) // 从尾部删除元素
            --back_pos;
        else {
            back_pos = N-1; // 回退到前一个chunk
            back_chunk = back_chunk->prev;
        }
        if(end_pos) // 以为这当前的chunk还有其他元素占有
            --end_pos;
        else {
            end_pos = N-1; // 当前chunk 没有元素占用
            end_chunk = end_chunk->prev;
            free(end_chunk->next);
            end_chunk->next = NULL;
        }
    }

    // 删除队列头部元素
    inline void pop() {
        if(++begin_pos == N) { //删除满一个chunk才回收一个chunk
            chunk_t *o = begin_chunk;
            beign_chunk = begin_chunk->next;
            begin_chunk->prev = NULL; 
            beign_pos = 0;

            chunk_t *cs = spare_chunk.xchg(o);
            free(cs);
        } 
    }

 对于ypipe.hpp文件中的内容腾讯的这边博客写的十分详细,这里附上链接因为个人画图能力有限且远没有别人讲解讲得好所以附上链接

无锁队列的几种实现及其性能对比-腾讯云开发者社区-腾讯云 (tencent.com)

总结

对于上述zmq无锁队列,适用于但消费者,单生产者,对于多消费者多生产者模型这里学习了一种循环数组无锁队列,同时针对上述zmq无锁队列其实对于数据量小于十万级别的数据提升并不大,一般适用于对于高吞吐量的时候才考虑使用无锁队列,同时对于zmq无锁队列几个关键技术点这里做如下总结。

1. zmq无锁队列封装了chunk_t 的数据结构,很好的解决了每次动态分配内存锁带来的开销。这里其实我们也可以自己实现一个内存池吧完全释放掉的chunk_t 放入内存池方便下次利用,避免重复申请,这里很好的解决了局部稳态性原理。

2. 利用CAS原子操作解决了无锁操作,同时无锁队列解决了cache 失效问题,不使用无锁队列时,线程被切换的时候,如果导致cache 不足出现失效的问题。而无锁队列采用原子操作实现无锁并发方访问,保证了对共享数据访问的原子性,避免出现竞态条件或者数据不一致的问题。

3. ypipe_t支持调用者自己去实现 notify + condition wait的通知:写入的时候,如果之前为空,可以提示我们调用者发送notify,读取的时候,如果没有读取到数据,我们可以提示condition wait。

无锁队列还有一个比较难理解的问题:没有数据读取时,我们应该怎么办

这里提供三点思路,当然这三点不一定都是好的,仅仅是一个思路:

a. usleep,这个可能带来很多的问题,休眠了1 ms还没数据怎么办,如何设置合理的休眠时间,我们使用无锁队列时一般都是因为高吞吐量,那么休眠时间设置不合理,可能导致数据丢失的错误等等这种思路会出现很多问题。

b. mutex+notify+condition,什么时候发notify呢,如果每次都插入元素都发notify 会不会消耗很多时间呢,有时间会详细探索。

c. 提交数据之前判断是不是为空,在向无锁队列提交数据时,通常会先判断队列是否为空。如果队列为空,则直接将数据插入队列中。如果队列非空,则需要使用原子操作来插入数据,以避免多个线程同时插入数据的竞争问题。也可以使用条件变量唤醒阻塞线程,当队列为空的时候,需要等阿迪数据到来才能继续执行。在ZMQ的无锁对了实现汇总,通常会使用条件变量来唤醒线程。当队列为空时,阻塞线程会通过条件变量进入睡眠状态,等待新数据的到带来。一旦有了新数据就会被唤醒。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Qt 是一个跨平台的 C++ 应用程序开发框架,而 ZeroMQ(简称 ZMQ)是一个高性能、异步的消息传递库。在 Qt 中使用 ZMQ 实现消息队列需要进行以下几个步骤: 1. 首先,下载并安装 ZMQ 库。你可以从 ZeroMQ 的官方网站(https://zeromq.org/)下载适合你的操作系统的库文件,并按照安装指南进行安装。 2. 在 Qt 项目中添加 ZMQ 的头文件路径和库文件路径。在项目的 .pro 文件中添加以下内容: ```pro INCLUDEPATH += /path/to/zmq/include LIBS += -L/path/to/zmq/lib -lzmq ``` 将上述路径替换为你实际安装 ZMQ 库的路径。 3. 在 Qt 代码中引入 ZMQ 相关的头文件: ```cpp #include <zmq.hpp> ``` 4. 创建一个 ZMQ 的上下文(context)对象和一个 socket 对象: ```cpp zmq::context_t context(1); zmq::socket_t socket(context, ZMQ_PUB); // 这里以发布者(publisher)为例,如果是订阅者(subscriber)则使用 ZMQ_SUB ``` 5. 配置 socket 的连接参数(可选): ```cpp socket.bind("tcp://localhost:5555"); // 绑定到本地地址和端口 ``` 6. 发送消息到消息队列: ```cpp std::string message = "Hello, ZMQ!"; zmq::message_t zmqMessage(message.size()); memcpy(zmqMessage.data(), message.data(), message.size()); socket.send(zmqMessage); ``` 可以根据具体需求自定义消息的格式和内容。 7. 关闭 socket 和 context: ```cpp socket.close(); context.close(); ``` 这样,你就可以在 Qt 中使用 ZMQ 实现消息队列的功能了。当然,这只是一个简单的示例,实际应用可能需要更复杂的逻辑来处理接收和处理消息等操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值