前言
隊列作爲常用的數據結構,使用上較爲廣泛。以長連接通訊爲例,爲實現數據生成與發送處理上互不干涉,數據發送緩衝隊列是一個較爲常用的緩衝手段。在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網絡發送層要往多播網絡中不斷髮送包數據。在入隊與出隊的過程中,通過觀察隊列中的數據堆積情況,保留一定冗餘度的情況下,適時調整隊列的長度。