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的影响。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339