队列作为常用的数据结构,使用上较为广泛。以长连接通讯为例,为实现数据生成与发送处理上互不干涉,数据发送缓冲队列是一个较为常用的缓冲手段。在TPLine 投屏直播会议系统中,无论广播发送端还是广播接收端,都采用发送缓冲区实现数据生成后的缓冲发送工作。
入队与出队也通常运行在不同的线程中,为实现数据频繁的入队与出队操作而不出问题,锁定操作普通情况下是一种非常常用的手段。下面首先给出的就是以锁的方式实现队列的一些参考。以objective-c为例,在ios中没有线程wait/notifi的常用机制,取而代之的是以“信号量”的方式实现,这方面不作详细介绍了。Java标准库中已经实现这些具有并发访问且线程安全的API。
在TPLine 投屏直播会议系统开发的后期(因为项目太多,其实这个项目也就是有空的时候做一下而已),全面改用UDP的方式发送接收数据,为提升效率,改用了无锁队列作为发送缓冲。在下面会给出环形队列的部分代码。
队列的实现有多种方式,如链表和数组。这里以数组为例提供部分实现代码供大家参考。
//
// CacheQueue.m
// Demo
//
// Created by Adam on 2019/6/21.
// Copyright © 2019年 Adam. All rights reserved.
//
#import "CacheQueue.h"
@interface CacheQueue()
@property(nonatomic, strong)NSMutableArray *pool;
@property(nonatomic, strong)dispatch_queue_t pushQueue;
@property(nonatomic, strong)dispatch_semaphore_t popSemaphore;
@property(nonatomic, strong)dispatch_semaphore_t pushSemaphore;
@end
@implementation CacheQueue
- (instancetype)init{
self = [super init];
if (self){
self.pool = [[NSMutableArray alloc] init];
self.pushQueue = dispatch_queue_create("push_queue", DISPATCH_QUEUE_SERIAL);
self.popSemaphore = dispatch_semaphore_create(0);
self.pushSemaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)push:(id<NSObject>)item{
if (item != nil){
dispatch_async(self.pushQueue, ^{
dispatch_semaphore_wait(self.pushSemaphore, DISPATCH_TIME_FOREVER);
[self.pool addObject:item];
dispatch_semaphore_signal(self.popSemaphore);
});
}
}
- (id<NSObject>)pop{
dispatch_semaphore_wait(self.popSemaphore, DISPATCH_TIME_FOREVER);
if (self.pool.count > 0){
id<NSObject> item =[self.pool firstObject];
if (item != nil){
[self.pool removeObjectAtIndex:0];
dispatch_semaphore_signal(self.pushSemaphore);
return item;
}
}
return nil;
}
@end
可以注意到,在定义数组时看上去定义的是非原子的NSMutableArray,实际上的锁定操作是由 dispatch_semaphore_create 创建的信号量提供。
一提到无锁队列,很多人首先想到的就是环形队列。一般来说,环形队列提供固定长度的数组(以数组实现为例)为基础,通过两个游标进行移动操作,实现数据的入队与出队。
网络上太多以c/c++ 链表为基础实现的demo,而objective-c的实现相关例子就现时为止,还没有找到一个行之有效的demo。
以ios开发为例,如果直接使用c写的代码,在数据传播中要进行数据类型的转换。所以下面以objective-c为例给出我的实现方式,供大家参考。
//
// CycleQueue.m
// Demo
//
// Created by Adam on 2019/6/21.
// Copyright © 2019年 Adam. All rights reserved.
//
#import "CycleQueue.h"
#define DEFAULT_SIZE 128
@interface CycleQueue()
@property(nonatomic, strong)NSMutableArray *pool;
@property(nonatomic, assign)int frist;
@property(nonatomic, assign)int last;
@property(nonatomic, strong)dispatch_queue_t pushQueue;
@property(nonatomic, strong)dispatch_queue_t popQueue;
@property(nonatomic, strong)dispatch_semaphore_t popSemaphore;
@property(nonatomic, copy)NSString *dRef;
@end
@implementation CycleQueue
- (instancetype)init{
self = [super init];
if (self){
self.frist = 0;
self.last = 1;
self.dRef = @"";
self.pool = [[NSMutableArray alloc] initWithCapacity:DEFAULT_SIZE];
self.pool[0] = self.dRef; //第一个赋值的位置为1,所以0的位置要赋值
self.pushQueue = dispatch_queue_create("push_queue", DISPATCH_QUEUE_SERIAL);
self.popQueue = dispatch_queue_create("pop_queue", DISPATCH_QUEUE_SERIAL);
self.popSemaphore = dispatch_semaphore_create(0);
}
return self;
}
- (BOOL)isFull{
int next = (self.last + 1) % DEFAULT_SIZE;
if (next == self.frist){
NSLog(@" *** [isFull]");
return YES;
}
return NO;
}
- (BOOL)isEmpty{
int next = (self.frist + 1) % DEFAULT_SIZE;
if(next == self.last){
NSLog(@" *** [isEmpty]");
return YES;
}
return NO;
}
- (void)push:(id<NSObject>)item{
dispatch_async(self.pushQueue, ^{
if ([self isFull] == NO){
self.pool[self.last] = item;
self.last = (self.last + 1) % DEFAULT_SIZE;
dispatch_semaphore_signal(self.popSemaphore);
}
});
}
- (id<NSObject>)pop{
dispatch_semaphore_wait(self.popSemaphore, DISPATCH_TIME_FOREVER);
if ([self isEmpty] == NO){
int next = (self.frist + 1) % DEFAULT_SIZE;
id item = self.pool[next];
self.pool[next] = self.dRef;
self.frist = next;
return item;
}
return nil;
}
- (void)runLoop:(callBack)block{
dispatch_async(self.popQueue, ^{
while (true) {
id<NSObject> item = [self pop];
if (item != nil){
block(item);
}
}
});
}
@end
在这个环形队列中,信号量的运用只用在了出队的逻辑中。在进行出队操作时,当队列中没有的数据时,一直等待,当有数据时等待结束,运行外部逻辑。
而在数据插入的逻辑中没有使用到信号量的处理,所以入队操作不会因为并行因素而影响入队的性能。
可见在队列中数据存在的情况下,入队与出队操作在无锁的情况下运行,各司其职,互不干涉,这正是无锁队列的魅力所在。
由上面简单的代码得知,无论是入队游标还是出队游标,都是通过 (index + 1)% size 求余数所得。在运行处理上,往往对内存进行直接赋值的方式比运算后赋值来得快。所以(index + 1)% size 的求余运算可以通过条件判断加直接赋值的方式所替代,效率更高,速度更快。
队列的长度在不同业务逻辑上可设置的长度可进行调整。原则上只是为了协调入队操作与出队操作之间的关系。
以TPLine中的处理为例,一方面业务逻辑层在处理完数据的压缩之后,经过对包数据进行自定义协议的封装,再对数据进行逻辑分片再封包,把数据传入队列中。另一方面UDP网络发送层要往多播网络中不断发送包数据。在入队与出队的过程中,通过观察队列中的数据堆积情况,保留一定冗余度的情况下,适时调整队列的长度。