會議投屏直播:緩衝隊列與環形隊列

前言

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章