GCD(二) dispatch_barrier

本文是GCD多线程编程中dispatch_barrier内容的小结,通过本文,你可以了解到:

  • dispatch_barrier的来源
  • 如何使用dispatch_barrier_async/dispatch_barrier_sync函数处理栅栏任务
  • dispatch_barrier_async/dispatch_barrier_sync函数使用效果的对比
  • 如何使用dispatch_barrier_async实现多读单写

dispatch_barrier的来源

通过上一篇文章GCD(一) 队列、任务、串行、并发的讲解,我们了解到,并发队列可以让你追加到队列的block并发执行,而不需要等待前面入队的block完成运行。但是这样又会引发一个问题,如果并发队列允许所有的block同时执行,那么他们为什么被称为队列(FIFO)呢,它不更像一个可以加入并发执行block的堆吗?

针对上面的问题,GCD中提供了Dispatch_barrier系统的API,俗称栅栏,使用dispatch_barrier_sync()或者dispatch_barrier_async()入队的block,会等到所有的之前入队的block执行完成后才开始执行。除此之外,在barrier block后面入队的所有的block,会等到到barrier block本身已经执行完成之后才继续执行。它就像我们平时早上上班挤地铁限流的样子,一位地铁工作人员拿着一个指示牌,表示在他排在之前的人流(无序并行)进站之后,他之后的人流才可以进站。就是因为这个栅栏,并发队列的行为看起来就像队列了。

测试代码在这

dispatch_barrier_async

/*!
 * @functiongroup Dispatch Barrier API
 * The dispatch barrier API is a mechanism for submitting barrier blocks to a
 * dispatch queue, analogous to the dispatch_async()/dispatch_sync() API.
 * It enables the implementation of efficient reader/writer schemes.
 * Barrier blocks only behave specially when submitted to queues created with
 * the DISPATCH_QUEUE_CONCURRENT attribute; on such a queue, a barrier block
 * will not run until all blocks submitted to the queue earlier have completed,
 * and any blocks submitted to the queue after a barrier block will not run
 * until the barrier block has completed.
 * When submitted to a a global queue or to a queue not created with the
 * DISPATCH_QUEUE_CONCURRENT attribute, barrier blocks behave identically to
 * blocks submitted with the dispatch_async()/dispatch_sync() API.
 */

/*!
 * @function dispatch_barrier_async
 *
 * @abstract
 * Submits a barrier block for asynchronous execution on a dispatch queue.
 *
 * @discussion
 * Submits a block to a dispatch queue like dispatch_async(), but marks that
 * block as a barrier (relevant only on DISPATCH_QUEUE_CONCURRENT queues).
 *
 * See dispatch_async() for details.
 *
 * @param queue
 * The target dispatch queue to which the block is submitted.
 * The system will hold a reference on the target queue until the block
 * has finished.
 * The result of passing NULL in this parameter is undefined.
 *
 * @param block
 * The block to submit to the target dispatch queue. This function performs
 * Block_copy() and Block_release() on behalf of callers.
 * The result of passing NULL in this parameter is undefined.
 */

从它的官方注释中我们可以知道:

  1. dispatch_barrier 是一个类似于dispatch_async()/dispatch_sync()的API,它可以将barrier block提交到队列中,barrier block 只有提交到自定义的并发队列中才能真正的当做一个栅栏,它在这里起到一个承上启下的作用,只有比它(barrier block)先提交到自定义并发队列的block全部执行完成,它才会去执行,等它执行完成,在它之后添加的block才会继续往下执行。
  2. dipatch_barrier block没有被提交到自定义的串行队列中,它与dispatch_async()/dispatch_sync()的作用是一样的。

我们通过一些代码去验证一下:

##pragma mark - dispatch_barrier_async + 自定义并发队列
/*
 * 特点:
 * 1.barrier之前的任务并发执行,barrier之后的任务在barrier任务完成之后并发执行
 * 2.会开启新线程执行任务
 * 3.不会阻塞当前线程(主线程)
 */
- (IBAction)executeBarrierAsyncCustomConcurrentQueueTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"---begin---");
    
    NSLog(@"追加任务1");
    dispatch_async(self.concurrentQueue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    NSLog(@"追加任务2");
    dispatch_async(self.concurrentQueue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    NSLog(@"追加barrier_async任务");
    dispatch_barrier_async(self.concurrentQueue, ^{
        // 追加barrier任务
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"barrier---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    NSLog(@"追加任务3");
    dispatch_async(self.concurrentQueue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    NSLog(@"追加任务4");
    dispatch_async(self.concurrentQueue, ^{
        // 追加任务4
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    NSLog(@"---end---");
    NSLog(@"*********************************************************");
}

执行结果如下:

2019-04-23 16:14:35.900776+0800 GCD(二) dispatch_barrier[18819:3551551] CurrentThread---<NSThread: 0x600002b76980>{number = 1, name = main}
2019-04-23 16:14:35.900984+0800 GCD(二) dispatch_barrier[18819:3551551] ---begin---
2019-04-23 16:14:35.901171+0800 GCD(二) dispatch_barrier[18819:3551551] 追加任务1
2019-04-23 16:14:35.901355+0800 GCD(二) dispatch_barrier[18819:3551551] 追加任务2
2019-04-23 16:14:35.901596+0800 GCD(二) dispatch_barrier[18819:3551551] 追加barrier_async任务
2019-04-23 16:14:35.901789+0800 GCD(二) dispatch_barrier[18819:3551551] 追加任务3
2019-04-23 16:14:35.902093+0800 GCD(二) dispatch_barrier[18819:3551551] 追加任务4
2019-04-23 16:14:35.902378+0800 GCD(二) dispatch_barrier[18819:3551551] ---end---
2019-04-23 16:14:35.902644+0800 GCD(二) dispatch_barrier[18819:3551551] *********************************************************
2019-04-23 16:14:37.904283+0800 GCD(二) dispatch_barrier[18819:3551647] 1---<NSThread: 0x600002b11bc0>{number = 3, name = (null)}
2019-04-23 16:14:37.904283+0800 GCD(二) dispatch_barrier[18819:3552871] 2---<NSThread: 0x600002b11cc0>{number = 4, name = (null)}
2019-04-23 16:14:39.909809+0800 GCD(二) dispatch_barrier[18819:3551647] 1---<NSThread: 0x600002b11bc0>{number = 3, name = (null)}
2019-04-23 16:14:39.909810+0800 GCD(二) dispatch_barrier[18819:3552871] 2---<NSThread: 0x600002b11cc0>{number = 4, name = (null)}
2019-04-23 16:14:41.914667+0800 GCD(二) dispatch_barrier[18819:3552871] barrier---<NSThread: 0x600002b11cc0>{number = 4, name = (null)}
2019-04-23 16:14:43.917811+0800 GCD(二) dispatch_barrier[18819:3552871] barrier---<NSThread: 0x600002b11cc0>{number = 4, name = (null)}
2019-04-23 16:14:45.921840+0800 GCD(二) dispatch_barrier[18819:3552871] 3---<NSThread: 0x600002b11cc0>{number = 4, name = (null)}
2019-04-23 16:14:45.921847+0800 GCD(二) dispatch_barrier[18819:3551647] 4---<NSThread: 0x600002b11bc0>{number = 3, name = (null)}
2019-04-23 16:14:47.927349+0800 GCD(二) dispatch_barrier[18819:3552871] 3---<NSThread: 0x600002b11cc0>{number = 4, name = (null)}
2019-04-23 16:14:47.927373+0800 GCD(二) dispatch_barrier[18819:3551647] 4---<NSThread: 0x600002b11bc0>{number = 3, name = (null)}

由此我们可以看出:

在barrier_async任务之前加入队列的任务,会在barrier任务之前并发执行,并且开辟了2条新线程去执行,barrier任务在任务1、任务2执行完成之后执行,执行完成之后,后续添加的任务才继续往下执行,并且dispatch_async并没有阻塞当前的主线程

dispatch_barrier_sync

我们将上一步的测试代码中的dispatch_barrier_async改为dispatch_barrier_sync方法去执行,得到的log如下:

2019-04-23 16:18:04.874397+0800 GCD(二) dispatch_barrier[18819:3551551] CurrentThread---<NSThread: 0x600002b76980>{number = 1, name = main}
2019-04-23 16:18:04.874601+0800 GCD(二) dispatch_barrier[18819:3551551] ---begin---
2019-04-23 16:18:04.874758+0800 GCD(二) dispatch_barrier[18819:3551551] 追加任务1
2019-04-23 16:18:04.874929+0800 GCD(二) dispatch_barrier[18819:3551551] 追加任务2
2019-04-23 16:18:04.875118+0800 GCD(二) dispatch_barrier[18819:3551551] 追加barrier_sync任务
2019-04-23 16:18:06.880055+0800 GCD(二) dispatch_barrier[18819:3552872] 1---<NSThread: 0x600002b12100>{number = 5, name = (null)}
2019-04-23 16:18:06.880102+0800 GCD(二) dispatch_barrier[18819:3562466] 2---<NSThread: 0x600002b288c0>{number = 6, name = (null)}
2019-04-23 16:18:08.885244+0800 GCD(二) dispatch_barrier[18819:3552872] 1---<NSThread: 0x600002b12100>{number = 5, name = (null)}
2019-04-23 16:18:08.885244+0800 GCD(二) dispatch_barrier[18819:3562466] 2---<NSThread: 0x600002b288c0>{number = 6, name = (null)}
2019-04-23 16:18:10.886126+0800 GCD(二) dispatch_barrier[18819:3551551] barrier---<NSThread: 0x600002b76980>{number = 1, name = main}
2019-04-23 16:18:12.887616+0800 GCD(二) dispatch_barrier[18819:3551551] barrier---<NSThread: 0x600002b76980>{number = 1, name = main}
2019-04-23 16:18:12.887776+0800 GCD(二) dispatch_barrier[18819:3551551] 追加任务3
2019-04-23 16:18:12.887907+0800 GCD(二) dispatch_barrier[18819:3551551] 追加任务4
2019-04-23 16:18:12.888021+0800 GCD(二) dispatch_barrier[18819:3551551] ---end---
2019-04-23 16:18:12.888121+0800 GCD(二) dispatch_barrier[18819:3551551] *********************************************************
2019-04-23 16:18:14.888428+0800 GCD(二) dispatch_barrier[18819:3552872] 4---<NSThread: 0x600002b12100>{number = 5, name = (null)}
2019-04-23 16:18:14.888461+0800 GCD(二) dispatch_barrier[18819:3562492] 3---<NSThread: 0x600002b29e00>{number = 7, name = (null)}
2019-04-23 16:18:16.893977+0800 GCD(二) dispatch_barrier[18819:3562492] 3---<NSThread: 0x600002b29e00>{number = 7, name = (null)}
2019-04-23 16:18:16.893977+0800 GCD(二) dispatch_barrier[18819:3552872] 4---<NSThread: 0x600002b12100>{number = 5, name = (null)}

通过log可以看到,barrier_syncbarrier_async一样,都可以在并发队列中起到栅栏的作用。但是2个方法还是有些不同的,下文中我们会详细讲解

dispatch_barrier_async与dispatch_barrier_sync的对比

通过上面的代码测试,我们可以发现,barrier_asyncbarrier_sync的区别仅仅在于,barrier_sync会阻塞它之后的任务的入队,必须等到barrier_sync任务执行完毕,才会把后面的异步任务添加到并发队列中,而barrier_async不需要等自身的block执行完成,就可以把后面的任务添加到队列中。

dispatch_barrier_sync与死锁

由于只有使用自定义并发队列时,dispatch_barrier方式添加的任务,才能起到栅栏的作用,添加到其它队列的情况下,dispatch_barrier_async/dispatch_barrier_syncdispatch_async/dispatch_sync的作用是一样的,所以,当在串行队列中使用dispatch_barrier_sync时,同样的也有可能死锁,所以,我们在平常开发中要谨慎使用dispatch_barrier_sync

dispatch_barrier_async实现多读单写

假如说我们在内存维护一个字典或者是一个DB文件,有多个读者或者写者都要操作这个共享数据,我们为了实现这个多读单写的模型,就需要考虑多线程对这个数据的访问问题,我们首先要解决读者与读者应该是并发的读取,代表了多读的含义,其次呢,读者与读者应该是互斥的,比如说,有读者在读取数据的时候,就不能有些的线程去写数据,所以说,读者与写者要互斥,其次呢,写者与写者也要互斥,有一个写线程在写数据,那么另一个写线程就不能去写数据,否则会导致程序异常或者程序错乱,要满足一下三点,其实我们可以使用dispatch_barrier_async来实现多读单写

  • 读者与读者并发
  • 读者与写着互斥
  • 写者与写者互斥

我们再通过一副图来看一下多读单写的具体实现流程


dispatch_barrier_multi_read_single_write.png

假如说有多个读处理同时或者并发执行的话,然后写处理需要跟读处理互斥操作,在写处理完成之后呢,然后可以再次进行读取处理,而dispatch_barrier_async正好就为我们实现了一个多读单写的模型,也就是当我们的读者在进行读处理的时候,其它的读者也可以额进行读取,但是此时,不能进行写,如果在写操作的过程中,有其它的读处理,那么这些读处理,就只能在写操作完成之后才可以执行。

  1. 我们首先定义一个类ZEDMultiReadSingleWriteHandler,然后在类中定义2个属性
/** 并发队列 */
@property (nonatomic, strong) dispatch_queue_t concurrentQueue;

/** 多读单写的数据容器,可能在不同的线程中访问 */
@property (nonatomic, strong) NSMutableDictionary *dict;

第一个属性是一个自定义的并发队列,用于使用dispatch_barrier_async的方式进行写操作。

第二个属性是一个数据存储的容器,可能会在不同的线程中访问。

  1. 在类的初始化方法里,创建并发队列与全局容器字典
- (instancetype)init {
    self = [super init];
    if (self) {
        self.concurrentQueue = dispatch_queue_create("zed.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
        self.dict = [NSMutableDictionary dictionary];
    }
    return self;
}
  1. 然后我们定义2个方法,用于给外部调用的读操作与写操作
- (id)dataForKey:(NSString *)key;
- (void)setData:(id)data forKey:(NSString *)key;
  1. 对于读操作而言,多个读操作可以并发执行,并发队列的特性就是允许提交的任务并发执行,我们这里提交的任务是通过一个key去字典中获取对象,由于这个获取操作是需要立刻同步返回结果的,所以我们要通过dispatch_sync这个函数来进行调用,同步立刻返回这个调用结果,同步到这个并发队列中,就可以允许多个线程同时读取,比如说,dataForKey这个方法可以在A线程中调用,也可以在B线程中调用,当A线程与B线程并发来访问这个同一个Key的时候,由于并发队列的性质,就可以保证他们同时并发读取某一个key的值,同时是同步调用,所以不管是哪一个线程,都可以通过这种同步方式立刻返回调用结果。

    - (id)dataForKey:(NSString *)key {
        __block id data;
        //同步读取指定数据
        dispatch_sync(self.concurrentQueue, ^{
            data = [self.dict objectForKey:key];
        });
        return data;
    }
    
  2. 写的操作就是就通过dispatch_barrier_async到一个并发队列当中去进行写,然后我们通过key把对应的值写进去

    - (void)setData:(id)data forKey:(NSString *)key {
        // 异步栅栏调用设置数据
        dispatch_barrier_async(self.concurrentQueue, ^{
            [self.dict setObject:data forKey:key];
        });
    }
    

如果文中有错误的地方,或者与你的想法相悖的地方,请在评论区告知我,我会继续改进,如果你觉得这个篇文章总结的还不错,麻烦动动小手,给我的文章与Git代码样例点个✨

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

推荐阅读更多精彩内容