优秀数据结构学习 - 共享内存无锁队列的实现(二)

1 关键技术

1.1 基于共享内存的存储和通信

操作系统提供的进程间通信机制有文件、socket、消息队列、管道、共享内存等。其中,共享内存是最快的IPC机制[6]。

共享内存映射到进程空间后,数据可以直接从共享内存进行读写,不需要执行系统调用进行数据的传输。因此,避免了其它进程间通信机制必须的用户态/内核态切换以及用户空间与内核空间的数据拷贝。

由于消息队不需支持跨主机通信,所以可以采用共享内存进行进程间的通信。

1.2 无锁互斥访问

消息队列需要支持多个写者,在多个写者同时进行写操作时,会产生并发操作。处理并发操作,通常的解决方案是使用互斥锁。使用互斥锁简单方便,但有一定的系统开销。在我们的测试环境下,单次锁/解锁操作耗时大概为20纳秒。多个线程互斥操作时,时间开销大大增加。同样的测试条件,当两个线程并发操作时,单次锁/解锁操作的平均时延为320纳秒。

如果并发操作能限制在一个机器字节内,可以使用CPU提供的CAS指令,即’Compare& Swap’原子操作,进行进程间的互斥操作。

相较于使用互斥锁进行多进程间的互斥操作,使用CAS指令的程序“临界区”更小,只有一个字节。当多个写者同时更新“临界区”时,只有一个写者成功,其它写者需要重复操作并检查结果。这样避免了进程锁起时休眠,以及锁操作带来的开销。最大程度的降低响应时延。

无锁程序的关键在于程序设计时,将程序的”临界区”设置为一个机器字节的变量,进而可以使用CAS指令进行原子操作。

1.2.1 消息队列多写者的无锁操作

在消息队列中,数组和数组的头尾head、tail指针是多进程访问的临界区。

只考虑写者,操作临界区为向数组写入数据、tail指针的更新:

critical section begin
	write to A[tail]
	tail++
critical section end

减少临界区内容,在临界区只对tail指针进行修改:写者竞争获取到tail针后,再写入数据:

critical section begin
	s = tail
	tail++
critical section end
	write to A[s]

现在,临界区只有tail一个变量,使用CAS指令代替互斥锁:

s = tail
do{
	next tail = s+1;
	ret = CAS(&tail, s, next_tail);
	if(s == ret)
		break;
	s = ret;
}while(1);
write to A[s];

1.2.2 队列读操作

由于只有一个读者,因此不需考虑head指针并发操作的问题。但由于写者先更新tail指针内容,后写入数据,读者在读数据前需要先判断数据是否有效。

消息队列的消息格式如下:

struct {
	volatile unsigned int data_len;
	char data[0];
};

写者写入数据前,data_len为0,写入数据后,更新data_len内容。此时,读者才开始读数据。读者读取数据后,重置data_len为0;这样,下次写者写入该位置数据完成前,读者不会提前读取。

2 消息队列设计

消息队列不是作为独立的服务,而是设计成库的形式提供使用。应用程序调用消息队列库的接口,编译时将库文件链接到目标文件即可。

按照功能划分,共享内存消息队列划分为消息队列、消息队列创建、消息队列销毁、数据读取、数据写入、消息队列查询等模块,各模块对应的功能如下:

  • 消息队列: 提供基础数据结构,缓存写入的数据,供其他模块读取或者查询。

  • 消息队列创建: 创建指定键值、大小和容量的共享内存消息队列。

  • 消息队列销毁: 销毁指定键值的共享内存消息队列,释放共享内存。

  • 数据读取: 读取共享内存消息队列中的数据。

  • 数据写入: 向共享内存消息队列中写入数据。

  • 消息队列查询: 查询共享内存消息队列的使用情况。

2.1 模块组成

2.1.1 总体组成

共享内存消息队列的组成如下图,其中消息队列由消息队列创建模块创建,供数据读写模块、消息队列查询模块使用,最终被消息队列销毁模块销毁释放。

在这里插入图片描述

2.1.2 消息队列组成

消息队列是一个基于共享内存的环形队列,在开辟的一块连续的共享内存中,保存消息队列相关信息,缓存写入的数据。它在内存中的结构定义:

在这里插入图片描述

消息队列头包含队列大小、消息记录大小,head、tail位置指针,用于读写同步的条件变量和锁等控制信息。环形队列分别使用首指针head和尾指针tail标记队列的头和尾,读者从head指向的位置读取数据,写者向tail指向的位置写入数据。

在这里插入图片描述

2.2 消息队列模块设计

2.2.1 创建和销毁

消息队列应该在应用进程启动前创建,在应用进程退出后再销毁。创建和销毁由独立的程序调用创建和销毁模块进行操作。

创建消息队列过程主要包括:

    1. 根据参数确定消息长度、个数,计算队列缓存大小
    2. 申请共享内存
    3. 初始化消息队列头部控制信息,初始化队列缓存

销毁消息队列过程主要包括:

	1. 销毁消息队列头部控制信息
	2. 释放共享内存其中控制信息主要包括读者、写者同步需要的条件变量和互斥锁。

2.2.2 数据写入

消息队列支持多个写者同时写操作,多个写者对队列的竞争写由CAS操作完成。写操作的过程如下:

   1. 检查环形队列空闲数量是否大于临界值
   2. 如果是,执行3;如果否,超时等待,继续执行1
   3. 获取当前tail值,new_tail为tail+1
   4. 通过CAS指令,使用new_tail更新tail
   5. 如果成功,根据new_tail位置写入数据,然后更新该位置消息的data_len为实际数据长度;如果不成功,跳到1继续执行
   6. 如果有读者在等待,通知读者

值得注意的是,写者先更新tail指针,后写入数据。这样才能保证多个写者通过CAS进行互斥操作。但是tail指针更新后,不保证数据已经更新完成。读者在读数据时需要根据data_len的值判断数据是否完整。

2.2.3 数据读取

消息队列只有一个读者,逻辑比较简单,读操作过程如下:

 1. 检查环形队列是否有数据
 2. 如果是,head自加1;如果否,超时等待,执行1
 3. 检查环形队列head位置的数据data_len是否为0
 4. 如果是,超时等待,若超过500毫秒data_len仍为0,认为写者异常,跳过该位置,执行1;如果否,读取数据
 5. 更新该位置data_len为0
 6. 如果有写者在等待,通知写者

2.2.4 读写同步

读者和写者使用条件变量进行同步。条件变量保存在消息队列头部信息中。只有当写者因没有空闲位置进入等待状态时,读者读取数据后才会发送条件变量信号进行通知。同样,只有当读者因没有数据而进入等待状态时,写者写入数据后才发送条件变量信号进行通知。

详细介绍:https://mp.weixin.qq.com/s/RqHsX3NIZ4_BS8O30KWYhQ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值