GCD的栅栏函数的原理及使用

什么是栅栏函数

在GCD中的栅栏函数有dispatch_barrier_async(异步)和dispatch_barrier_sync(同步),异步不会阻塞当前线程,反之则会阻塞当前线程。在GCD中的并行队列中,栅栏函数起到一个栅栏的作用,它等待队列所有位于barrier函数之前的操作执行完毕后执行,并且在barrier函数执行之后,barrier函数之后的操作才会得到执行,该函数需要同dispatch_queue_create函数生成的DISPATCH_QUEUE_CONCURRENT队列一起使用。
代码示例:

dispatch_queue_t conque1 = dispatch_queue_create("com.helloworld.djx1", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(conque1, ^{
        NSLog(@"1");
    });
    
    dispatch_async(conque1, ^{
        NSLog(@"2");
    });
    dispatch_barrier_async(conque1, ^{
        NSLog(@"3");
    });
    dispatch_async(conque1, ^{
        NSLog(@"4");
    });

打印结果:

2021-08-24 22:42:06.498806+0800 GCDDemo[14589:25112631] 1
2021-08-24 22:42:06.498923+0800 GCDDemo[14589:25112629] 2
2021-08-24 22:42:06.499488+0800 GCDDemo[14589:25112629] 3
2021-08-24 22:42:06.500302+0800 GCDDemo[14589:25112629] 4

任务3必须等到1、2都执行完之后才能执行4。

栅栏函数的使用

栅栏函数的这个特点,使得它非常适合用于做多读单写读写锁。比如说对于一个数据,可以多线程读取,但是只能单线程修改,就非常适合用栅栏函数dispatch_barrier和同步函数dispatch_sync配合并行队列做数据的读写安全机制。下面就以实现可变数组安全读写机制为例,来演示dispatch_barrier的用法。

读写锁的实现
  • 1、首先创建一个NSMutableArray的Category,用于自定义可变数组安全操作方法(用Category不用继承的原因是不愿意迫害数组原来的方法,而且我们对数组实现源码不了解,不好把握)。
@interface NSMutableArray (SafeOp)

- (NSInteger)safe_count;

- (id)safe_objectAtIndex:(NSUInteger)index;

- (NSUInteger)safe_indexOfObject:(id)anObject;

- (void)safe_addObject:(id)anObject;

- (void)safe_insertObject:(id)anObject atIndex:(NSUInteger)index;

- (void)safe_removeLastObject;

- (void)safe_removeObjectAtIndex:(NSUInteger)index;

- (void)safe_removeAllObjects;

- (void)safe_removeObject:(id)anObject;

@end
  • 2、创建一个并行队列operationQueue,用于数组的读写操作队列。operationQueue是个单利,避免创建大量的队列;
- (dispatch_queue_t)operationQueue {
    static dispatch_queue_t queue = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        queue = dispatch_queue_create("com.djx.GCDDemo.NSMutableArray", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_set_specific(queue, kSafeMutableArrayQueueSpecific, kSafeMutableArrayQueueSpecific, NULL);
    });
     return queue;
}
  • 3、使用dispatch_barrier_sync(为什么不用异步?后面会有解释)实现数组的写锁操作,使用dispatch_sync函数实现读操作。
static inline void safe_op_arr_write(dispatch_queue_t queue, void (^block)(void)){
    dispatch_barrier_sync(queue, ^{
        block();
    });
}

static inline id safe_op_arr_read(dispatch_queue_t queue, id (^block)(void)){
    __block id data = nil;
    dispatch_sync(queue, ^{
        data = block();
    });
    return data;
}

这里使用内联函数inline是希望尽量提高效率。

  • 4、实现自定义方法安全读写
    在读书数据时调用safe_op_arr_read(dispatch_sync),增删改时调用safe_op_arr_write(dispatch_barrier_sync)。具体实现代码如下:
@implementation NSMutableArray (SafeOp)

#pragma mark - private method
- (dispatch_queue_t)operationQueue {
    static dispatch_queue_t queue = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        queue = dispatch_queue_create("com.djx.GCDDemo.NSMutableArray", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_set_specific(queue, kSafeMutableArrayQueueSpecific, kSafeMutableArrayQueueSpecific, NULL);
    });
     return queue;
}

- (NSInteger)safe_count
{
    NSNumber *countNum = safe_op_arr_read(self.operationQueue, ^id{
    
        return @(self.count);
    });
    return [countNum integerValue];
}

- (id)safe_objectAtIndex:(NSUInteger)index
{
    id object = safe_op_arr_read(self.operationQueue, ^id{
        if (index >= self.count) {
            return nil;
        }
        return [self objectAtIndex:index];
    });
    return object;
}

- (NSUInteger)safe_indexOfObject:(id)anObject
{
    NSNumber *indexNum = safe_op_arr_read(self.operationQueue, ^id{
        NSInteger index = [self indexOfObject:anObject];
        return @(index);
    });
    return [indexNum integerValue];
}

- (void)safe_addObject:(id)anObject
{
    if (!anObject) {
        return;
    }
    safe_op_arr_write(self.operationQueue, ^{
        [self addObject:anObject];
    });
}

- (void)safe_removeLastObject
{
    safe_op_arr_write(self.operationQueue, ^{
        [self removeLastObject];
    });
}

- (void)safe_removeObjectAtIndex:(NSUInteger)index
{
    safe_op_arr_write(self.operationQueue, ^{
        if (index < self.count) {
            [self removeObjectAtIndex:index];
        }
    });
}

- (void)safe_removeObject:(id)anObject
{
    safe_op_arr_write(self.operationQueue, ^{
        if (anObject) {
            [self removeObject:anObject];
        }
    });
}

- (void)safe_removeAllObjects
{
    safe_op_arr_write(self.operationQueue, ^{
        [self removeAllObjects];
    });
}
@end
可变数组安全读写代码使用示范

为了验证线程安全,我们同时创建一个并行队列和一个全局并发队列,制造复杂的队列和线程环境,反复执行,目前没有出现死锁、异常的情况。

    //并发队列
    dispatch_queue_t conque = dispatch_queue_create("com.helloworld.djx", DISPATCH_QUEUE_CONCURRENT);

    //全局并发队列
    dispatch_queue_t globQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    self.array = [[NSMutableArray alloc] init];
    NSInteger listCount = 10000;
    for (int i = 0; i < listCount; i++) {
        dispatch_async(conque, ^{
            [self.array safe_addObject:@(i)];
            NSLog(@"2arrCount:%ld", [self.array safe_count]);
        });
    }

    for (int i = 0; i < listCount; i++) {
        dispatch_async(globQueue, ^{
            [self.array safe_removeObjectAtIndex:i];
            NSLog(@"2arrCount:%ld", [self.array safe_count]);
        });
    }
为什么栅栏函数不使用dispatch_barrier_async而是dispatch_barrier_sync?

1、首先是从性能上来考虑,对数组的写操作时间往往非常短暂,不至于对线程造成长久堵塞,而如果使用异步函数dispatch_barrier_async则可能会开辟新的线程,相对于数数组的操作,开辟新的线程的时间、内存损耗可能更大,得不偿失;
2、dispatch_barrier_async和dispatch_sync操作同一个并行队列会导致死锁,死锁的原因在对数据边写边读的时候,由于dispatch_barrier_async是异步,在dispatch_barrier_async往队列添加任务(block)时,在还没执行的时候它会立即返回执行下面的代码,而此时dispatch_sync准备同步执行任务(block),但是这时候由于队列中有一个栅栏任务,dispatch_sync必须等dispatch_barrier_async的任务执行完才能执行,而dispatch_barrier_async中的任务的执行也要dispatch_sync执行完成才能继续往下执行代码,此时造成死锁。下面是死锁的代码:

 NSMutableArray *arr = [NSMutableArray array];
    dispatch_queue_t conque1 = dispatch_queue_create("com.helloworld.djx1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t conque2 = dispatch_queue_create("com.helloworld.djx2", DISPATCH_QUEUE_CONCURRENT);
    for ( int i = 0; i < 100; i ++) {
        dispatch_async(conque1, ^{

            dispatch_barrier_async(conque2, ^{
                [arr addObject:@(i)];
            });
            dispatch_sync(conque2, ^{
                NSLog(@"i:%d-%@", i, @(arr.count));
            });
        });
    }

这个代码是会死锁的。

栅栏函数使用注意事项

为什么不能跟全局并行队列配合使用呢?原因在于全局队列属于系统创建并管理,这个队列不止我们app在用,系统也在用。里面很多涉及到系统自身相关的操作,一旦我们外部app阻塞这个队列,有可能会影响系统相关的操作。因此栅栏函数对全局(globa)并行队列的操作是无效的,比如下面的demo:

dispatch_queue_t conque1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_async(conque1, ^{
        NSLog(@"1");
    });
    dispatch_async(conque1, ^{
        NSLog(@"2");
    });
    dispatch_barrier_async(conque1, ^{
        NSLog(@"3");
    });
    dispatch_async(conque1, ^{
        NSLog(@"4");
    });

这里的队列是全局并行队列,1、2的打印顺序不一定是在3之前,不受3的影响。

栅栏函数为什么不能跟串行队列一起用?因为是多余的。本身串行队列就已经是一个执行完成才能执行下一个,所以根本就不需要栅栏函数。

你可能感兴趣的:(GCD的栅栏函数的原理及使用)