什么是栅栏函数
在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的影响。
栅栏函数为什么不能跟串行队列一起用?因为是多余的。本身串行队列就已经是一个执行完成才能执行下一个,所以根本就不需要栅栏函数。