• 手撕环形队列


    【摘要】 环形队列,是一种非常高效的数据结构,在操作系统、数据库、中间件和各种应用系统中大量使用。今天咱们就来盘它。下面是一个环形队列的示意图:环形队列,有两个指针:头指针和尾指针。在队尾写入,移动尾指针;从队列头部读取,移动头指针。环形队列,是一种特殊的队列。因此具有队列的显著特征:先进先出。其特殊性在于"环形", 内存空间可以不断重复使用,无需频繁分配和释放内存。并且,可以非常容易实现无锁的数据结...

    环形队列,是一种非常高效的数据结构,在操作系统、数据库、中间件和各种应用系统中大量使用。今天咱们就来盘它。

     

    下面是一个环形队列的示意图:

    2021-07-29_01.jpg


    环形队列,有两个指针:头指针和尾指针。在队尾写入,移动尾指针;从队列头部读取,移动头指针。

    环形队列,是一种特殊的队列。因此具有队列的显著特征:先进先出。

    其特殊性在于"环形", 内存空间可以不断重复使用,无需频繁分配和释放内存。并且,可以非常容易实现无锁的数据结构,在多生产者多消费者的多线程并发场景中,效率非常高。

    今天,我们先来实现一个最基本的环形队列。后续文章,再不断为其增加更加强悍的特性。

     

    通常,我们用一个数组来实现环形队列。数组内存,一次性分配好,写入超过数组末尾时,会回绕至数组开始位置继续写入。读取至数组尾部也会回绕。

    需要重点解决的问题是:
    当头指针和尾指针相遇时,需要准确判断出,环形队列是空,还是满,从而决定是否可以继续写入,是否能够继续读取。

     

    我们用一个变量 same_cycle,来完成对环形队列空/满的判断。具体逻辑如下:
    1)初始,head = 0, tail = 0,都指向环形队列的位置0处。我们把head或tail指针,在环形队列中转了完整一圈,叫一个轮次。初始,same_cycle = 1(true), 表示head和tail 两个指针是同一轮次的。
    2)写入时,如果队列已满,则无法写入,直接返回失败。如果队列未满,则在tail 位置写入,tail移动至下一个位置(可能会回绕)。如果下一个位置为数组位置0,则表示开始了一个新的轮次,因此设置 same_cycle = 0(false)。
    3)读取时,如果队列已空,则无法读取,直接返回失败。如果队列未空,则从head位置读取,head移动至下一个位置(可能会回绕)。如果下一个位置为数组位置0,则表示开始了一个新的轮次,与tail指针的轮次变得相同,因此设置 same_cycle = 1(true)。

    根据以上,环形队列为空的判断规则为:
    (head == tail) && same_cycle 

    环形队列已满的判断规则为:
    (head == tail) && !same_cycle 


    环形队列,C语言实现的代码如下:

    // ring_queue.h
    #ifndef RING_QUEUE_H
    #define RING_QUEUE_H
    
    typedef struct ring_queue_t {
        char* pbuf;
        int item_size;
        int capacity;
    
        int head;
        int tail;
        int same_cycle;
    } ring_queue_t;
    
    int ring_queue_init(ring_queue_t* pqueue, int item_size, int capacity);
    void ring_queue_destroy(ring_queue_t* pqueue);
    
    int ring_queue_push(ring_queue_t* pqueue, void* pitem);
    int ring_queue_pop(ring_queue_t* pqueue, void* pitem);
    
    int ring_queue_is_empty(ring_queue_t* pqueue);
    int ring_queue_is_full(ring_queue_t* pqueue);
    
    #endif

     

    // ring_queue.c
    
    #include "ring_queue.h"
    
    #include <stdlib.h>
    #include <string.h>
    
    int ring_queue_init(ring_queue_t* pqueue, int item_size, int capacity) {
        memset(pqueue, 0, sizeof(*pqueue));
        pqueue->pbuf = (char*)malloc(item_size * capacity);
        if (!pqueue->pbuf) {
            return -1;
        }
    
        pqueue->item_size = item_size;
        pqueue->capacity = capacity;
        pqueue->same_cycle = 1;
        return 0;
    }
    
    void ring_queue_destroy(ring_queue_t* pqueue) {
        free(pqueue->pbuf);
        memset(pqueue, 0, sizeof(*pqueue));
    }
    
    int ring_queue_push(ring_queue_t* pqueue, void* pitem) {
        if (ring_queue_is_full(pqueue)) {
            return -1;
        }
    
        memcpy(pqueue->pbuf + pqueue->tail * pqueue->item_size, pitem, pqueue->item_size);
        pqueue->tail = (pqueue->tail + 1) % pqueue->capacity;
        if (0 == pqueue->tail) {    // a new cycle
            pqueue->same_cycle = 0;     // tail is not the same cycle with head
        }
    
        return 0;
    }
    int ring_queue_pop(ring_queue_t* pqueue, void* pitem) {
        if (ring_queue_is_empty(pqueue)) {
            return -1;
        }
    
        memcpy(pitem, pqueue->pbuf + pqueue->head * pqueue->item_size, pqueue->item_size);
        pqueue->head = (pqueue->head + 1) % pqueue->capacity;
        if (0 == pqueue->head) {
            pqueue->same_cycle = 1;     // head is now the same cycle with tail
        }
    
        return 0;
    }
    
    int ring_queue_is_empty(ring_queue_t* pqueue) {
        return (pqueue->head == pqueue->tail) && pqueue->same_cycle;
    }
    
    int ring_queue_is_full(ring_queue_t* pqueue) {
        return (pqueue->head == pqueue->tail) && !pqueue->same_cycle;
    }


    写个测试程序,验证一下:

    // test_ring_queue.c
    #include "ring_queue.h"
    #include <stdio.h>
    
    static void test_push(ring_queue_t* pq, int val);
    static void test_pop(ring_queue_t* pq);
    
    int main() {
        ring_queue_t queue, *pq = &queue;
        int iret = ring_queue_init(pq, sizeof(int), 3);
        iret = ring_queue_is_empty(pq);
        printf("ring_queue is%s empty!\n", iret ? "" : " not");
    
        int val = 1;
        test_push(pq, val++);
        test_push(pq, val++);
        test_push(pq, val++);
        test_pop(pq);
        test_push(pq, val++);
    
        iret = ring_queue_is_full(pq);
        printf("ring_queue is%s full!\n", iret ? "" : " not");
    
        test_push(pq, val++);
    
        test_pop(pq);
        test_pop(pq);
        test_pop(pq);
        test_pop(pq);
    
        return 0;
    }


    编译,运行这个测试程序,输出结果为:

    $ ./test_ring_queue
    ring_queue is empty!
    ring_queue_push succ, val = 1
    ring_queue_push succ, val = 2
    ring_queue_push succ, val = 3
    ring_queue_pop succ, val = 1
    ring_queue_push succ, val = 4
    ring_queue is full!
    ring_queue_push failed! iret = -1
    ring_queue_pop succ, val = 2
    ring_queue_pop succ, val = 3
    ring_queue_pop succ, val = 4
    ring_queue_pop failed! iret = -1

    ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    【摘要】 本文是手撕环形队列系列的第二篇,之前的文章链接如下:《手撕环形队列》前面文章介绍的是一个比较基本的环形队列,能够在多线程中使用,但有一个前提:任意时刻,生产者和消费者最多都只能有一个。也就是说,如果有多个生产者要并发向队列中写入,需要在外部进行加锁或其它方式的并发控制,保证任意时刻最多只有一个生产者真正向环形队列进行写入。同样的,多个消费者要从队列中读取进行消费,也需要在外部进行加锁或其它方...

    前面文章介绍的是一个比较基本的环形队列,能够在多线程中使用,但有一个前提:

    任意时刻,生产者和消费者最多都只能有一个。

    也就是说,如果有多个生产者要并发向队列中写入,需要在外部进行加锁或其它方式的并发控制,保证任意时刻最多只有一个生产者真正向环形队列进行写入。同样的,多个消费者要从队列中读取进行消费,也需要在外部进行加锁或其它方式的并发控制,保证任意时刻最多只有一个消费者从环形队列进行读取。


    本文的内容,就是介绍如何能够支持多线程场景下,多生产者并发写入、多消费者并发读取,完全由环形队列内部来解决,无需外部做任何额外的控制。并且,使用无锁的技术来实现,从而避免加锁解锁这种重操作对性能的影响。


    无锁数据结构中,主要的技术实现手段是使用cpu的原子指令。介绍原子指令之前,先介绍一下没有原子指令的情况下会有什么问题。


    通常我们在程序源码中写的语句,编译为二进制后,代码中的一行文本语句会变成二进制的多条汇编指令,因此这一行文本语句cpu执行时就不是原子的。多行文本语句,就更不是原子的了。多线程并发执行这些文本语句时,对应的多行汇编语句会在多个cpu 核上同时执行,无法保证他们之间的执行先后顺序关系。在多线程同时读写一个共享数据时,会发生各种误判,导致错误的结果。


    以环形队列为例,来说明这个问题:
    环形队列为初始状态,队列为空。两个生产者线程都要向队列进行写入,都调用 ring_queue_push()方法。这个方法的函数实现中,producer1 线程读取tail 为0,producer2 线程也读取到tail为0。然后producer1 向位置0写入数据,然后把tail 增1,tail变为1。
    producer2 也向位置0写入数据,然后把tail 增1. tai增加1的过程:
    tail = tail + 1; 
    由于producer2 初始读取的tail 值为0,这个cpu core 可能意识不到tail 已经被别的线程修改了,因此还认为tail是0,因此最终
    tail = 0 + 1 = 1;

    最终的结果,producer2 把producer1的数据给覆盖了(数据丢了),但两个ring_queue_push()函数调用都返回成功了。这是一个严重的Bug!


    实际多线程环境中,各个cpu 之间的代码执行时序都是不同的,因此没有任何防护的情况下,对同样的内存位置写入、对同一个变量的并发读和并发写,都会产生严重的Bug。

     

    为了解决这些问题,原子指令闪亮登场了!

    用这些指令,对数据的操作在多cpu的情况下也是原子性的。所谓原子性,就是作为执行的最小单位,不能再分割。cpu core 要么执行了这个指令,要么还没执行这个指令。不会出现在一个cpu core 执行这个指令一半的时候,另外一个cpu core开始执行这个指令的情况。


    通过正确使用cpu的原子指令,能够有效解决多线程并发中的各种问题。


    在解决多线程并发问题,常规的方法是用mutex、semaphore、condvar等,这些可以理解为粗粒度锁,使用简单,适用范围广,但性能较差。
    cpu的原子指令,是cpu指令级的细粒度锁,性能非常高,但设计起来复杂。


    各种操作系统、开发语言中都提供了对cpu原子指令的包装函数,因此不需要我们手写汇编指令。

    以gcc为例,gcc提供了 一系列builtin 的原子函数,比如今天我们要用的:

    bool __sync_bool_compare_and_swap(type *ptr, type oldval, type newval);

    这个函数,会将 ptr指向内存中的值,与oldval 比较,如果相等,则把 ptr指向内存的值修改为 newval. 整个比较和修改的全过程,要以原子方式完成。如果比较相等,并且修改成功,则返回true。其它情况都返回false。
    这个函数,也叫 cas,取的是 compare and swap 三个单词的首字母缩写。

     

    我们用原子指令,来增强一下环形队列,实现多生产多消费者并发读写。思路如下:
    对于写入,每个producer 必须先获得写锁。成功获得写锁之后,写入数据,将tail移动到下一个位置,最后释放写锁。

    对于读取,每个consumer 必须先获得读锁。成功获得读锁之后,读取数据,将head移动到下一个位置,最后释放读锁。


    整个思路,与传统通过mutex控制对共享数据的读写是完全一样的,只是技术实现上我们用原子指令来实现,这种实现方式叫无锁数据结构。


    另外,需要说明的是:
    对于head和tail这样的变量,由于多个线程会并发读写,因此我们需要用 volatile 来修饰它们,不让cpu core 缓存它们,避免读到旧数据。


    无锁环形队列,支持多生产者多消费者并发读写,用C语言实现的源码如下:

    // ring_queue.h
    #ifndef RING_QUEUE_H
    #define RING_QUEUE_H
    ​
    typedef struct ring_queue_t {
        char* pbuf;
        int item_size;
        int capacity;
    ​
        volatile int write_flag;
        volatile int read_flag;
    ​
        volatile int head;
        volatile int tail;
        volatile int same_cycle;
    } ring_queue_t;
    ​
    int ring_queue_init(ring_queue_t* pqueue, int item_size, int capacity);
    void ring_queue_destroy(ring_queue_t* pqueue);
    int ring_queue_push(ring_queue_t* pqueue, void* pitem);
    int ring_queue_pop(ring_queue_t* pqueue, void* pitem);
    int ring_queue_is_empty(ring_queue_t* pqueue);
    int ring_queue_is_full(ring_queue_t* pqueue);
    ​
    #endif

     

    // ring_queue.c
    #include "ring_queue.h"
    ​
    #include <stdlib.h>
    #include <string.h>
    ​
    #define CAS(ptr, old, new) __sync_bool_compare_and_swap(ptr, old, new)
    ​
    int ring_queue_init(ring_queue_t* pqueue, int item_size, int capacity) {
        memset(pqueue, 0, sizeof(*pqueue));
        pqueue->pbuf = (char*)malloc(item_size * capacity);
        if (!pqueue->pbuf) {
            return -1;
        }
    ​
        pqueue->item_size = item_size;
        pqueue->capacity = capacity;
        pqueue->same_cycle = 1;
        return 0;
    }
    ​
    void ring_queue_destroy(ring_queue_t* pqueue) {
        free(pqueue->pbuf);
        memset(pqueue, 0, sizeof(*pqueue));
    }
    ​
    ​
    int ring_queue_push(ring_queue_t* pqueue, void* pitem) {
        // try to set write flag
        while (1) {
            if (ring_queue_is_full(pqueue)) {
                return -1;
            }
    ​
            if (CAS(&pqueue->write_flag, 0, 1)) {   // set write flag successfully
                break;
            }
        }
    ​
        // push data
        memcpy(pqueue->pbuf + pqueue->tail * pqueue->item_size, pitem, pqueue->item_size);
        pqueue->tail = (pqueue->tail + 1) % pqueue->capacity;
        if (0 == pqueue->tail) {    // a new cycle
            pqueue->same_cycle = 0;     // tail is not the same cycle with head
        }
    ​
        // reset write flag
        CAS(&pqueue->write_flag, 1, 0);
    ​
        return 0;
    }
    ​
    int ring_queue_pop(ring_queue_t* pqueue, void* pitem) {
        // try to set read flag
        while (1) {
            if (ring_queue_is_empty(pqueue)) {
                return -1;
            }
    ​
            if (CAS(&pqueue->read_flag, 0, 1)) {    // set read flag successfully
                break;
            }
        }
    ​
        // read data
        memcpy(pitem, pqueue->pbuf + pqueue->head * pqueue->item_size, pqueue->item_size);
        pqueue->head = (pqueue->head + 1) % pqueue->capacity;
        if (0 == pqueue->head) {
            pqueue->same_cycle = 1;     // head is now the same cycle with tail
        }
    ​
        // reset read flag
        CAS(&pqueue->read_flag, 1, 0);
    ​
        return 0;
    }
    ​
    int ring_queue_is_empty(ring_queue_t* pqueue) {
        return (pqueue->head == pqueue->tail) && pqueue->same_cycle;
    }
    ​
    int ring_queue_is_full(ring_queue_t* pqueue) {
        return (pqueue->head == pqueue->tail) && !pqueue->same_cycle;
    }
    ------------------------------------------------------------------------------------------------------------------------------------------------------

    转自华为云社区:

    手撕环形队列-云社区-华为云 (huaweicloud.com)

    手撕环形队列系列二:无锁实现高并发-云社区-华为云 (huaweicloud.com)

  • 相关阅读:
    java 20.抽象类和抽象方法
    java 19.继承
    测试用例生成器工具
    PICT测试用例生成工具
    mysql笔记
    java 17.数组工具类Arrays
    java 13. 方法重载构造方法块this用法
    java 16.静态static
    map area canvas 自定义图片热区
    springbootswagger
  • 原文地址:https://www.cnblogs.com/heluan/p/16434031.html
Copyright © 2020-2023  润新知