队列

概念

队列是模拟一组人排队办事行为的数组结构
队列的直观体验:https://www.cs.usfca.edu/~galles/visualization/QueueArray.html
image

顺序队列

队列采用顺序存储结构时,可利用一维数组来存放节点数据,在数组中ucArray[]中存放队列。

head与tail的引入

由于队列的操作只能在队头和队尾上进行,且不能移动队列中的节点,因此必须有活动的队头和队尾的索引。
图中ucHead为头索引,指向一个满节点ucTail为尾索引,指向一个空节点
image

用C语言描述

#define MAXSIZE (128)                   //队列的最大长度
typedef struct _queue_t_
{
    unsigned char ucArray[MAXSIZE];     //所用的内存空间
    unsigned char ucHead;               //队头索引
    unsigned char ucTail;               //队尾索引
}queue_t;

出队时

ucHead ++;

入队时

ucArray[ucTail ++] = x;

不足

当其入队到最后时,要做一次整体搬移动作,以腾出空间。
数据搬移用时会较长,影响入队速度。
而循环队列则不会出现此问题

循环队列(ring-buffer)

什么是循环队列

ucTail=MAXSIZE-1时,即已达到队列空间最末处时,再次入队时,ucTail=0,重新指向队列空间的开始位置。

一般情况

image

队空

image

队满

image

如何判断队空队满

由上图的情况可以知道,ucHead与ucTail已经无法区分,故需要对现有标识物进行改造。
现在引入队列含有节点的个数icount
当队列为空时,icount=0;
当队列为满时,icount=MAXSIZE;
当入队时,icount++;
当出队时,icount--

故循环队列的数据结构可以描述为:

#define MAXSIZE (128)                   //队列的最大长度
typedef struct _ring_queue_t_
{
    unsigned char ucArray[MAXSIZE];     //所用的内存空间
    unsigned char ucHead;               //队头索引
    unsigned char ucTail;               //队尾索引
    unsigned int  iCount;               //有效数据个数
}ring_queue_t;

其实,ucTail=ucHead+iCount。但是会出现ucTail > MAXSIZE -1的情况。故有ucTail=(ucHead+iCount)%MAXSIZE
为了减小数据结构占用的空间,可将上面的数据结构优化为:

#define MAXSIZE (128)                   //队列的最大长度
typedef struct _ring_queue_t_
{
    unsigned char ucArray[MAXSIZE];     //所用的内存空间
    unsigned char ucHead;               //队头索引
    unsigned int  iCount;               //有效数据个数
}ring_queue_t;

如何入队出队

入队

unsigned char ucTail = (ucHead + iCount) % MAXSIZE;
ucArray[ucTail] = x;
icount ++;

出队

ucHead++;
ucHead %= MAXSIZE
iCount --;

队列基本操作

1. 创建
2. 销毁
3. 入队
4. 出队
5. 清空队列
6. 判断队列为空
7. 判断队列为满
8. 获得队列长度
9. 读取队列某一位置的元素值

循环队列的接口与实现

https://gitee.com/hany_li/hany_gear_lib/tree/master/ring_buffer

队列的应用

  1. 解决主机与外设之间速度不匹配的问题,比如主机与打印机,可设置一个打印数据缓冲区。
  2. 解决由多用户引起的资源竞争问题,比如CPU资源的竞争,OS按照每个请求的先后顺序,排成一个队列,依次处理。
  3. 方便进行事件驱动设计。
    事件驱动是一种简单而强大的程序设计方法,其思路是在事件发生时,将事件处理函数及参数保存到事件队列中。
    而main()函数里则不断地查询事件队列中是否有未处理的事件,如果有,将其出队并调用其处理函数。
    事件驱动程序设计的main()函数仅仅是不断地调用事件检测函数event_check()和事件队列处理函数do_event();
    其参考代码如下:
int main(void)
{
	queue_adt queue;
	
	//创建一个事件队列
	queue=new_event_queue();
	
	for(;;)
	{
	//事件检测函数
	event_check(queue);
	//事件队列处理函数
	do_event(queue);
	}

	//销毁指定事件队列
	destroy_event_queue(queue);
	return 0;
}

通过上面的分析可知,事件驱动程序设计的关键是设计事件队列处理代码。由于已经有了队列代码,因此可重用队列代码,简化事件队列设计。
创建和删除事件队列可以直接利用队列中创建和销毁函数。用#define重新定义一个名字即可。即:

#define event_queue_create() rb_create()
#define event_queue_destroy() rb_destroy()

事件队列处理代码最主要的部分是事件入队和处理入队的事件(即事件出队),其关键是用什么表示事件。
一般来说,一个事件必须有代码来处理它,否则这个事件对程序就没有意义。
而C语言中表示独立代码段的是函数,因此可以用指向事件处理函数的函数指针事件处理函数所需的参数代表这个事件。
为了举例方便,事件处理函数统一定义为无返回值,只有一个参数的函数,如下:

struct queue_event
{
	void (*event_fn)(void *p_arg);
	void *p_arg;
}

那么入队函数接口如下:

void event_queue_in(rb_adt_t rb, void (*event)(void *p_arg), void *p_arg)

void event_queue_in(rb_adt_t rb, struct queue_event * event_instance)

那么什么时候调用事件入队呢?
就在检测事件event_check()中,若发现了新的事件,就将其入队即可。
优点:能依次处理多个不同的事件,如按键事件,时间事件等等,每个事件由具体的函数与参数来明确其行为。

posted @ 2021-09-13 15:54  海林的菜园子  阅读(79)  评论(0编辑  收藏  举报