iOS实录:GCD使用小结(一)

2017-07-28iOS开发

导语:在iOS中,多线程方案有四种:pthread、NSThread、NSOperation & NSOperationQueue 和 GCD,但是开发中GCD使用得最多,本文主要总结一下我使用GCD的情况。

一、GCD(Grand Central Dispatch)概述

1、基本概念

GCD允许程序将任务切分为多个单一任务提交至Dispatch Queue,然后系统调度线程,实现并发或者串行地执行任务。GCD隐藏了内部线程的调度,开发者只需要关注创建或获取队列,然后将Block追加到队列中即可。

在iOS中有两种队列,分别是串行队列并发队列

串行队列:同一时间队列中只有一个任务在执行,每个任务只有在前一个任务执行完成后才能开始执行。主队列(通过dispatch_get_main_queue()获取,提交至主队列的任务会在主线程中执行) 就是串行队列,也可以使用dispatch_queue_create创建串行队列。

串行队列.png

并发队列:这些任务会按照被添加的顺序开始执行。但是任意时刻有多个Block(任务)运行,这个完全是取决于GCD。并发队列可以使用dispatch_queue_create创建,也可以获取进程中的全局队列,全局队列有:高、中(默认)、低三个优先级队列。可以调用dispatch_get_global_queue函数传入相应优先级来访问队列。

并发队列.png

同步执行:阻塞当前线程,直到当前block中任务执行完毕才返回。同步并不创建新线程。不能使用sync将任务添加到主队列,这样会造成死锁。

异步执行:不会阻塞当前线程,函数会立即返回, block会在后台异步执行;异步必定会开启新线程。

说明1:有些博客中将并发队列说成并行队列,这是不对的。因为并行是多个事件在同一时刻发生,而并发是多个事件在同一时间间隔发生;并行完全依赖处理器的核数。而并发才能充分的利用处理器的每一个核,以达到最高的处理性能。

说明2队列不等于线程。 作为开发者的我们,只是将Block添加进合适的GCD队列,真正的线程的调度是由系统完成的;无论同步(sync)还是异步(async)向主队列提交Block,最终Block都是在主线程中执行;同步(sync)往非主队列中提交Block,会在当前线程中执行; 如果是异步(async)往非主队列中提交Block,则会在分线程中执行。

2、串行队列/并发队列 和 同步/异步的组合 ( 重点 )

有四种组合方式

1)串行队列 + 同步组合(常用)

dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);//    dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", NULL);//dispatch_sync(serialQueue, ^{    NSLog(@"串行队列 + 同步:%@",[NSThread currentThread]);});dispatch_sync(serialQueue, ^{    NSLog(@"串行队列 + 同步:%@",[NSThread currentThread]);});dispatch_sync(serialQueue, ^{    NSLog(@"串行队列 + 同步:%@",[NSThread currentThread]);});

串行队列 + 同步组合结果.png

说明1:串行队列 (自己创建的串行线程)+ 同步组合下,不会新建线程,依然在当前线程上执行任务。不可以在主线程中使用sync方法,会造成死锁。

说明2:比较常用,同步锁的替代方法。

2)串行队列 + 异步组合

dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);//    dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", NULL);//dispatch_async(serialQueue, ^{    NSLog(@"串行队列 + 异步:%@",[NSThread currentThread]);});dispatch_async(serialQueue, ^{    NSLog(@"串行队列 + 异步:%@",[NSThread currentThread]);});dispatch_async(serialQueue, ^{    NSLog(@"串行队列 + 异步:%@",[NSThread currentThread]);});

串行队列 + 异步组合结果.png

说明:串行队列(无论是自己创建的,还是获取主队列) + 异步组合下,会新建线程,但只开启一条线程;

3)并发队列 + 同步组合

dispatch_queue_tconcurrentQueue =dispatch_queue_create("com.concurrent.queue",DISPATCH_QUEUE_CONCURRENT);dispatch_sync(concurrentQueue,^{    NSLog(@"并发队列 + 同步1:%@",[NSThread currentThread]);});dispatch_sync(concurrentQueue,^{    NSLog(@"并发队列 + 同步2:%@",[NSThread currentThread]);});dispatch_sync(concurrentQueue,^{    NSLog(@"并发队列 + 同步3:%@",[NSThread currentThread]);});

并发队列 + 同步组合结果.png

说明: 并发队列(无论是自己创建的,还是获取全局队列) + 同步组合下,并没有新建线程,任务依然在当前线程上执行。

4)并发队列 + 异步组合(常用)

dispatch_queue_tconcurrentQueue =dispatch_queue_create("com.concurrent.queue",DISPATCH_QUEUE_CONCURRENT);dispatch_async(concurrentQueue,^{    NSLog(@"并发队列 + 异步:%@",[NSThread currentThread]);});dispatch_async(concurrentQueue,^{    NSLog(@"并发队列 + 异步:%@",[NSThread currentThread]);});dispatch_async(concurrentQueue,^{    NSLog(@"并发队列 + 异步:%@",[NSThread currentThread]);});

并发队列 + 异步组合结果.png

说明:并发队列(无论是自己创建的,还是获取全局队列) + 异步组合下,会新建线程,iOS 系统中可以开多条线程。

同步异步

串行队列1、不会新建线程,依然在当前线程上执行任务;2、类似同步锁,是同步锁的替代方案1、会新建线程,但只开启一条线程;2、每次使用 dispatch_queue_create创建串行队列,就会创建一条新线程;多次创建,会创建多条线程,多条线程间并发执行。

并发队列不会新建线程,依然在当前线程上执行任务1、会新建线程,可以开多条线程;2、iOS7-SDK 时代一般是5、6条, iOS8-SDK 以后可以50、60条

总结1:不可以在主线程中使用sync方法,否则会造成死锁。

总结2:串行队列 + 同步组合 可以替代同步锁;

总结3:为了提高效率,如多线程下载图片等,并发队列 + 异步比较常用。

二、GCD使用1:异步处理

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);dispatch_async(globalQueue, ^{    // 一个异步的任务,如网络请求,耗时的文件操作等等    ...    dispatch_async(dispatch_get_main_queue(), ^{        // UI刷新 或其他主线程操作        ...    });});

说明:该用法最常见,如开启一个异步的网络请求,待数据返回后在主线程刷新UI等。

三、GCD使用2:单例

dispatch_once实现单例,以实现QSAccountManager单例为例。源码如下:

1、实现

//QSAccountManager.m@implementationQSAccountManagerstaticQSAccountManager *_shareManager =nil;+ (instancetype)shareManager{staticdispatch_once_tonce;dispatch_once(&once, ^{        _shareManager = [[selfalloc] init];    });return_shareManager;}+ (instancetype)allocWithZone:(struct_NSZone *)zone{staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{        _shareManager = [superallocWithZone:zone];    });return_shareManager;}- (nonnullid)copyWithZone:(nullableNSZone*)zone{return_shareManager;}@end

说明1:dispatch_once函数中,参数1是代码块是否被调用的谓词,参数2是被调用的代码块。该函数中的代码块只会被执行一次,而且还是线程安全的。

说明2:要保证单例类只有一个唯一的实例,还需要实现allocWithZone和copyWithZone方法,这保证使用init和copy方法返回也是唯一实例。

2、使用

QSAccountManager *account1 = [QSAccountManagershareManager];QSAccountManager *account2 = [QSAccountManager new];QSAccountManager *account3 = [[QSAccountManager alloc]init];QSAccountManager *account4 = [account3 copy];NSLog(@"account1 = %@",account1);NSLog(@"account2 = %@",account2);NSLog(@"account3 = %@",account3);NSLog(@"account4 = %@",account4);

单例输出结果.png

四、GCD使用3:代替同步锁

atomic的内存管理语义是原子性的,仅保证了属性的setter和getter方法是原子性的,但是执行效率低,可以使用GCD实现。

@synchronized(self)同步块机制,会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕,才释放锁。执行效率低。

替代方案:将数据的读取和写放入串行同步队列,保证数据同步,线程安全。

替代方案:结合GCD的栅栏块(barrier)和  并发队列 实现数据同步,线程安全。(比串行同步队列方式更高效)

1、代替atomic实现线程安全的setter和getter方法

//串行队列_syncQueue = dispatch_queue_create("com.jzp.syncQueue",NULL);//假设属性是someString- (NSString*)someString {    __blockNSString*localSomeString;dispatch_sync(_syncQueue, ^{        localSomeString = _someString;    });returnlocalSomeString;}- (void)setSomeString:(NSString*)someString {dispatch_sync(_syncQueue, ^{        _someString = someString;    });}

2、实现线程安全的NSMutableArray

主要依靠栅栏块单独执行的特性,在并发队列中如果发现接下来要处理的块是个栏栅块,那么就一直等到当前所有并发块都执行完毕,才会单独执行这个栏栅块。待栏栅块执行过后,再按正常方式继续向下处理。

这部分实现详见 iOS实录12:NSMutableArray使用中忽视的问题中“一、线程安全的NSMutableArray”

说明:dispatch_barrier_sync和dispatch_barrier_async只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。

五、GCD使用4:dispatch_group实现线程同步

1、简单模式

dispatch_queue_tqueue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);dispatch_group_tgroup= dispatch_group_create();dispatch_group_async(group,queue, ^{// 任务1});dispatch_group_async(group,queue, ^{// 任务2});// 等待group中多个异步任务执行完毕,会发出同步信号// 方式1(会阻塞当前线程,group上任务都完成或超时等待就执行)dispatch_group_wait(group, DISPATCH_TIME_FOREVER);// ...// 方式2(不会阻塞当前线程,group上任务都完成,执行block中代码)dispatch_group_notify(group, dispatch_get_main_queue(), ^{// 任务完成后,在主队列中做一些操作});

说明:将block(任务)放入队列中执行,并和调度组 group相关联;如果提交到dispatch queue中的block全都执行完毕,会执行dispatch_group_notify中的block代码; 或在group上任务完成前,dispatch_group_wait会阻塞当前线程(所以不能放在主线程调用)一直等待;当group上任务完成,或者等待时间超过设置的超时时间会结束等待。

2、多异步任务的同步

成对使用dispatch_group_enterdispatch_group_leave,可以将异步任务加入group中;

当这些异步任务处理完成后,dispatch_group_notify和dispatch_group_wait会收到同步信号;

异步任务如请求,通过该机制实现批量请求的处理。

dispatch_group_t batch_request_group = dispatch_group_create();dispatch_group_enter(batch_request_group);[self.request1 startWithCompleteBlock:^(BOOLisSuccess,id_Nullable responseObj,NSString* _Nonnull errorDesc) {//TODO 数据解析....dispatch_group_leave(batch_request_group);}];dispatch_group_enter(batch_request_group);[self.request2 startWithCompleteBlock:^(BOOLisSuccess,id_Nullable responseObj,NSString* _Nonnull errorDesc) {//TODO 数据解析....dispatch_group_leave(batch_request_group);}];dispatch_group_enter(batch_request_group);[self.request3 startWithCompleteBlock:^(BOOLisSuccess,id_Nullable responseObj,NSString* _Nonnull errorDesc) {//TODO 数据解析....dispatch_group_leave(batch_request_group);}];dispatch_group_notify(batch_request_group, dispatch_get_main_queue(), ^{//三个请求都结束了,继续处理});

六、GCD使用其他

1、dispatch_apply

按 指定的次数 将指定的Block追加到 指定的Dispatch Queue中, 并等到全部处理执行结束。有并行的运行机制,效率一般快于for循环的类串行机制。

/** @param10指定重复次数,这里指定10次 @param gQueue 追加对象的Dispatch Queue @paramindex带有参数的Block,index的作用是为了按执行的顺序区分各个Block */dispatch_apply(10, gQueue, ^(size_tindex) {    NSLog(@"%zu",index);});

2、dispatch_after

延迟执行

// NSEC_PER_SEC,每秒有多少纳秒。// USEC_PER_SEC,每秒有多少毫秒。// NSEC_PER_USEC,每毫秒有多少纳秒。// DISPATCH_TIME_NOW 从现在开始// DISPATCH_TIME_FOREVE 永久// time为5sdispatch_time_ttime = dispatch_time(DISPATCH_TIME_NOW,(int64_t)(5.0* NSEC_PER_SEC));dispatch_queue_tqueue= dispatch_get_main_queue();dispatch_after(time,queue, ^{// 在queue里面延迟执行的一段代码// ...});

3、小心死锁

死锁情况1:在主线程中使用sync方法

dispatch_sync(dispatch_get_main_queue(), ^{// 任务...});

死锁情况2:在串行队列添加同步任务;

// 在串行队列添加同步任务dispatch_sync(serialQueue, ^{// 任务dispatch_sync(serialQueue, ^{// 任务});};

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

推荐阅读更多精彩内容