会议投屏直播:缓冲队列与环形队列

目录

  • 前言
  • 缓冲队列
  • 环形队列
    • 代码实现
    • 性能优化
    • 队列的长度

前言

队列作为常用的数据结构,使用上较为广泛。以长连接通讯为例,为实现数据生成与发送处理上互不干涉,数据发送缓冲队列是一个较为常用的缓冲手段。在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网络发送层要往多播网络中不断发送包数据。在入队与出队的过程中,通过观察队列中的数据堆积情况,保留一定冗余度的情况下,适时调整队列的长度。

你可能感兴趣的:(ios,直播)