Dispatch Barrier栅栏函数
简介
在并发调度队列中执行的任务的同步点
使用栅栏函数来同步调度队列中一个或多个任务的执行。在向并发调度队列添加栅栏时,该队列会延迟栅栏任务(以及栅栏之后提交的所有任务)的执行,直到所有先前提交的任务都执行完成为止。在完成先前的任务后,队列将自己执行栅栏任务。栅栏任务执行完毕之后,队列将恢复其正常执行行为。
栅栏函数
同步栅栏函数
dispatch_barrier_sync
:提交一个栅栏 block 以同步执行,并等待该 block 执行完;
dispatch_barrier_sync_f
:提交一个栅栏 function 以同步执行,并等待该 function 执行完。
异步栅栏函数
dispatch_barrier_async
:提交一个栅栏 block 以异步执行,并直接返回;
dispatch_barrier_async_f
:提交一个栅栏 function 以异步执行,并直接返回。
同步栅栏函数 dispatch_barrier_sync
1 void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
dispatch_barrier_sync
同步栅栏函数会等待该函数传入的队列中的任务都执行完毕,再执行dispatch_barrier_sync
函数中的任务以及后面的任务,会阻塞当前线程。
需求: 现有任务1、2、3、4,前两个任务执行完毕,再执行后两个任务以及主线程的代码。
解决方法:
使用 GCD 队列组;
使用dispatch_barrier_sync
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 dispatch_queue_t queue = dispatch_queue_create("myqueue" , DISPATCH_QUEUE_CONCURRENT); dispatch_async (queue, ^{ for (NSUInteger i = 0 ; i < 3 ; i++) { NSLog (@"执行任务1-%zd-%@" ,i,[NSThread currentThread]); } }); dispatch_async (queue, ^{ for (NSUInteger i = 0 ; i < 3 ; i++) { NSLog (@"执行任务2-%zd-%@" ,i,[NSThread currentThread]); } }); dispatch_barrier_sync(queue, ^{ NSLog (@"------------------dispatch_barrier_sync-%@" ,[NSThread currentThread]); }); NSLog (@"任务1、2执行完毕" ); dispatch_async (queue, ^{ for (NSUInteger i = 0 ; i < 3 ; i++) { NSLog (@"执行任务3-%zd-%@" ,i,[NSThread currentThread]); } }); NSLog (@"正在执行任务3、4" ); dispatch_async (queue, ^{ for (NSUInteger i = 0 ; i < 3 ; i++) { NSLog (@"执行任务4-%zd-%@" ,i,[NSThread currentThread]); } });
异步栅栏函数 dispatch_barrier_async
1 void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
先来看一下上面的代码改为异步栅栏函数的执行效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 dispatch_queue_t queue = dispatch_queue_create("myqueue" , DISPATCH_QUEUE_CONCURRENT); dispatch_async (queue, ^{ for (NSUInteger i = 0 ; i < 3 ; i++) { NSLog (@"执行任务1-%zd-%@" ,i,[NSThread currentThread]); } }); dispatch_async (queue, ^{ for (NSUInteger i = 0 ; i < 3 ; i++) { NSLog (@"执行任务2-%zd-%@" ,i,[NSThread currentThread]); } }); dispatch_barrier_async(queue, ^{ NSLog (@"------------------dispatch_barrier_async-%@" ,[NSThread currentThread]); }); NSLog (@"任务1、2执行完毕" ); dispatch_async (queue, ^{ for (NSUInteger i = 0 ; i < 3 ; i++) { NSLog (@"执行任务3-%zd-%@" ,i,[NSThread currentThread]); } }); NSLog (@"正在执行任务3、4" ); dispatch_async (queue, ^{ for (NSUInteger i = 0 ; i < 3 ; i++) { NSLog (@"执行任务4-%zd-%@" ,i,[NSThread currentThread]); } });
从打印日志可以看到,改为异步栅栏函数,任务3、4仍然可以等到任务1、2以及栅栏任务都执行完毕再执行,但不会阻塞主线程,并且栅栏任务是在子线程执行。
dispatch_barrier_async
异步栅栏函数会等待该函数传入的队列中的任务都执行完毕,再执行dispatch_barrier_async
函数中的任务以及后面的任务,执行该函数会直接返回,继续往下执行代码,不会阻塞当前线程。
主要用于在多个异步操作完成之后,统一对非线程安全的对象进行更新;
适合于大规模的 I/O 操作;
当访问数据库或者文件的时候,更新数据的时候不能和其他更新或者读取的操作在同一时间执行,可以使用队列组不过有点复杂,可以使用dispatch_barrier_async解决;
保证线程安全、读写安全。
使用场景1:保证线程安全
例如: 我们要从网络上异步获取很多图片,然后将它们添加到非线程安全的对象——数组中去:异步并发。
同一时间点,可能有多个线程执行给数组添加对象的方法,所以可能会丢掉 1 到多次,我们执行 1000 次,可能数组就保存了 990 多个,还有程序出现奔溃的可能。
解决办法:
① 加锁:比较耗时,而且下载完什么时候添加进数组也不一定。我们希望所有图片都下载完,再往数组里面添加;
② 使用 GCD 队列组;
③ 使用dispatch_barrier_async函数,栅栏中的任务会等待队列中的所有任务执行完成,才会执行栅栏中的任务,保证了线程安全。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 dispatch_queue_t queue = dispatch_queue_create("myqueue" , DISPATCH_QUEUE_CONCURRENT); for (int i = 0 ; i < 5 ; i++) { dispatch_async (queue, ^{ NSLog (@"图片下载完成%d,%@" ,i,[NSThread currentThread]); dispatch_barrier_async(queue, ^{ NSLog (@"添加图片%d,%@" ,i,[NSThread currentThread]); }); }); }
使用场景2:实现读写安全
dispatch_barrier_async
可以用来实现“读写安全”。我们将“写”操作放在dispatch_barrier_async
中,这样能确保在“写”操作的时候会等待前面的“读”操作完成,而后续的“读”操作也会等到“写”操作完成后才能继续执行,提高文件读写的执行效率。
1 2 3 4 5 6 7 8 9 10 dispatch_queue_t queue = dispatch_queue_create("myqueue" , DISPATCH_QUEUE_CONCURRENT);dispatch_async (queue, ^{ }); dispatch_barrier_async(queue, ^{ });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 - (void )viewDidLoad { [super viewDidLoad]; self .queue = dispatch_queue_create("myqueue" , DISPATCH_QUEUE_CONCURRENT); for (int i = 0 ; i < 3 ; i++) { [test read]; [test read]; [test write]; [test read]; } } - (void )read { dispatch_async (self .queue, ^{ sleep(1 ); NSLog (@"read,%@" ,[NSThread currentThread]); }); } - (void )write { dispatch_barrier_async(self .queue, ^{ sleep(1 ); NSLog (@"write,%@" ,[NSThread currentThread]); }); }
dispatch_barrier_sync
与 dispatch_barrier_async
的区别及注意点
相同点: 都会等待队列中在它们(栅栏)之前提交的任务都执行完毕,再执行它们的任务,接着执行它们后面的任务。
不同点:
dispatch_barrier_sync
:提交一个栅栏 block 以同步执行,并等待该 block 执行完;由于是同步,不会开启新的子线程,会阻塞当前线程。
dispatch_barrier_async
:提交一个栅栏 block 以异步执行,并直接返回,会继续往下执行代码,不会阻塞当前线程。
注意点:
dispatch_barrier_(a)sync
函数传入的的队列必须是自己手动创建的并发队列,如果传入的是全局并发队列或者串行队列,那么这个函数是没有栅栏的效果的,效果等同于dispatch_(a)sync函数。
只能栅栏dispatch_barrier_(a)sync
函数中传入的queue。
Dispatch Apply多次执行
dispatch_apply
类似一个 for 循环。提交一个 block 到指定队列,并使该 block 执行指定的次数 n,等待 block 执行完 n 次才会返回。
1 2 3 4 5 6 7 8 9 10 11 12 void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));
注意点:
该函数会等待 block 都执行完毕才会返回,所以是同步的,会阻塞;
如果指定的队列是全局并发队列dispatch_get_global_queue
,则这些 block 可以并发执行,这里需要注意可重入性;
如果指定的队列是手动创建的并发队列,在有些情况下不会并发执行,所以建议使用全局并发队列dispatch_get_global_queue
;
当使用串行队列时,不会开启子线程,block 在主线程按串行执行;
当使用并发队列时,不一定会开启子线程,block 不一定都在子线程执行,也可能都在主线程执行,取决于任务的耗时程度。
串行队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - (void )test { dispatch_queue_t queue = dispatch_queue_create("myqueue" , DISPATCH_QUEUE_SERIAL); NSLog (@"开始执行" ); dispatch_apply(5 , queue, ^(size_t i) { sleep(1 ); NSLog (@"%zu,%@" ,i,[NSThread currentThread]); }); NSLog (@"执行完毕" ); }
并发队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - (void )test { dispatch_queue_t queue = dispatch_get_global_queue(0 , 0 ); NSLog (@"开始执行" ); dispatch_apply(5 , queue, ^(size_t i) { sleep(1 ); NSLog (@"%zu,%@" ,i,[NSThread currentThread]); }); NSLog (@"执行完毕" ); }
在某些场景下使用dispatch_apply
会对性能有很大的提升,比如你的代码需要以每个像素为基准来处理计算 image 图片。同时dispatch_apply能够避免一些线程爆炸的情况发生(创建很多线程)
1 2 3 4 5 6 7 8 for (int i = 0 ; i < 999 ; i++){ dispatch_async (q, ^{...}); } dispatch_barrier_sync(q, ^{}); dispatch_apply(999 , q, ^(size_t i){...});
在异步中实现同步,并将任务并发执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 dispatch_queue_t queue = dispatch_get_global_queue(0 , 0 ); dispatch_async (queue, ^{ NSLog (@"开始执行" ); dispatch_apply(5 , queue, ^(size_t i) { sleep(1 ); NSLog (@"%zu,%@" ,i,[NSThread currentThread]); }); NSLog (@"执行完毕" ); dispatch_async (dispatch_get_main_queue(), ^{ NSLog (@"回到主线程" ); }); });
将上面代码的队列改为手动创建的并发队列,任务就不会并发执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 dispatch_queue_t queue = dispatch_queue_create("myqueue" , DISPATCH_QUEUE_CONCURRENT); dispatch_async (queue, ^{ NSLog (@"开始执行" ); dispatch_apply(5 , queue, ^(size_t i) { sleep(1 ); NSLog (@"%zu,%@" ,i,[NSThread currentThread]); }); NSLog (@"执行完毕" ); dispatch_async (dispatch_get_main_queue(), ^{ NSLog (@"回到主线程" ); }); });
Dispatch Source
dispatch
框架提供一套接口用于监听系统底层对象(如文件描述符、Mach 端口、信号量等),当这些对象有事件产生时会自动把事件的处理 block
函数提交到 dispatch
队列中执行,这套接口就是 Dispatch Source API
,Dispatch Source
其实就是对 kqueue
功能的封装,可以去查看 dispatch_source
的 c 源码实现(什么是 kqueue
?Google,什么是 Mach 端口? Google Again),Dispatch Source 主要处理以下几种事件:
1 2 3 4 5 6 7 8 9 10 11 DISPATCH_SOURCE_TYPE_DATA_ADD 变量增加 DISPATCH_SOURCE_TYPE_DATA_OR 变量OR DISPATCH_SOURCE_TYPE_MACH_SEND Mach端口发送 DISPATCH_SOURCE_TYPE_MACH_RECV Mach端口接收 DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 内存压力情况变化 DISPATCH_SOURCE_TYPE_PROC 与进程相关的事件 DISPATCH_SOURCE_TYPE_READ 可读取文件映像 DISPATCH_SOURCE_TYPE_SIGNAL 接收信号 DISPATCH_SOURCE_TYPE_TIMER 定时器事件 DISPATCH_SOURCE_TYPE_VNODE 文件系统变更 DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像
当有事件发生时,dispatch source
自动将一个 block
放入一个 dispatch queue
执行。
GCD定时器
NSTimer
和 CADisplayLink
定时器不准时的问题,解决办法就是使用 GCD
定时器。GCD
的定时器是直接跟系统内核挂钩的,而且它不依赖于RunLoop,所以它非常的准时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 dispatch_queue_t queue = dispatch_queue_create("myqueue" , DISPATCH_QUEUE_SERIAL); dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0 , 0 , queue); uint64_t start = 2.0 ; uint64_t interval = 1.0 ; dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC ), interval * NSEC_PER_SEC , 0 ); dispatch_source_set_event_handler(timer, ^{ NSLog (@"%@" ,[NSThread currentThread]); }); dispatch_resume(timer); NSLog (@"%@" ,[NSThread currentThread]); self .timer = timer;
dispatch_queue_set_specific
& dispatch_get_specific
这两个 API 类似于objc_setAssociatedObject
跟objc_getAssociatedObject
,FMDB 里就用到这个来防止死锁,来看看 FMDB 的部分源码:
1 2 3 4 5 6 static const void * const kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey;_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@" , self ] UTF8String], NULL ); dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self , NULL );
当要执行数据库操作时,如果在 queue 里面的 block 执行过程中,又调用了 indatabase 方法,需要检查是不是同一个 queue,因为同一个 queue 的话会产生死锁情况
1 2 3 4 - (void )inDatabase:(void (^)(FMDatabase *db))block { FMDatabaseQueue *currentSyncQueue = (__bridge id )dispatch_get_specific(kDispatchQueueSpecificKey); assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock" ); }
dispatch_once_t
dispatch_once_t
必须是全局或static
变量
这一条算是“老生常谈”了,但我认为还是有必要强调一次,毕竟非全局或非static
的dispatch_once_t
变量在使用时会导致非常不好排查的bug,正确的如下:
1 2 3 4 5 static dispatch_once_t onceToken;dispatch_once (&onceToken, ^{ });
注意
onceToken实现单例时释放单粒 onceToken的作用域和赋值
dispatch_after
dispatch_after
是延迟提交,不是延迟运行
先看看官方文档的说明:
Enqueue a block for execution at the specified time.
Enqueue,就是入队,指的就是将一个Block在特定的延时以后,加入到指定的队列中,不是在特定的时间后立即运行!。
看看如下代面试题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 dispatch_queue_t queue = dispatch_queue_create("me.tutuge.test.gcd" , DISPATCH_QUEUE_SERIAL);NSLog (@"Begin add block..." );dispatch_async (queue, ^{ [NSThread sleepForTimeInterval:10 ]; NSLog (@"First block done..." ); }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC )), queue, ^{ NSLog (@"After..." ); });
结果如下:
1 2 3 2015 -03 -31 20 :57 :27.122 GCDTest[45633 :1812016 ] Begin add block...2015 -03 -31 20 :57 :37.127 GCDTest[45633 :1812041 ] First block done...2015 -03 -31 20 :57 :37.127 GCDTest[45633 :1812041 ] After...
参考
关于 GCD 的一些总结