iOS GCD使用详解

Grand Central Dispath(GCD):是iOS 4和OS X Snow Leopard 开始引入的新多线程编程功能。

GCD是异步执行任务的技术之一。

我们只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。

这里的线程管理是作为系统的一部分来实现的。

在GCD出现之前,Cocoa框架在NSObject类中提供了 performSelectorInBackground: withObject:方法和 performSelectorOnMainThread: withObject: waitUntilDone:方法等这样简单的多线程技术。

而GCD出现后,我们就可以通过 dispatch_async(queue, ^{ });和 dispatch_async(dispatch_get_main_queue(), ^{ });在后台和主线程中完成相应操作。

相比之下,GCD代码更为简洁,并且由于是系统管理线程,执行效率更高。

一个CPU核一次只能执行一个命令。CPU核执行的命令列也是一条无分叉的路径,即使地址分散在各处。

这种无分叉路径不只1条,存在多条时即为“多线程”。

多线程可能是一个CPU核在多个线程之间来回切换,看上去像一个CPU核并列地执行多个线程一样。也有可能是真的具有多个CPU核并行执行多个线程。

多线程可能发生的问题:

1、多个线程同时更新相同的资源,导致数据竞争;

2、多个线程相互等待,形成死锁;

3、使用太多线程会消耗大量内存;

但是多线程也必须使用,因为多线程可以保证应用程序的响应性能。但是长时间的处理不能在主线程中执行,需在其他线程中执行。因为在主线程中长时间处理会妨碍主循环RunLoop的执行,从而导致不能更新用户界面,应用程序的画面长时间停滞等问题。

GCD的API

“定义想执行的任务并追加到适当的Dispatch Queue中”。其中,Dispatch Queue就是执行处理任务的等待队列,而Block中就是待执行的任务。

在执行处理时,会用到两种队列:

1、Serial Dispatch Queue: 等待前一个任务执行完毕再开始处理下一个任务;

2、Concurrent Dispatch Queue: 不等待前一个任务处理完就开始执行下一个任务;

关于串行/并行,同步/异步的理解:

异步串行:就是另起一个新的线程,在这个新线程中,按照任务加入的顺序依次执行完所有的任务;

异步并行:就是创建多个新的线程,同时执行多个任务,因此,这个方式执行完任务的顺序是随机的;

同步并行:就是不创建新的线程,因此,即使是并行的队列,也必须按照任务添加的顺序依次执行完,才能继续后面的任务;

同步串行:和同步并行一样,不能开启新线程,只能按队列中任务的顺序来依次执行。

XNU内核(iOS和OS X的核心)决定了可以并行的线程最大数;XNU内核通过Concurrent Dispatch Queue来管理并执行多线程。

获取Dispatch Queue的方法:

1、通过 GCD的API:dispatch_queue_create生成Dispatch Queue。

2、获取系统标准提供的Dispatch Queue;

1、dispatch_queue_create

dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t _Nullable attr#>)

其中,label表示queue的名称,通常使用应用程序ID的逆序全称域名来表示。这样也方便程序在出现crash时,方便定位和调试。

第二个参数,attr用来指明要生成的队列类型,NULL和DISPATCH_QUEUE_SERIAL都表示生成串行队列,DISPATCH_QUEUE_CONCURRENT表示生成并行队列。

dispatch_queue_create函数可以生成任意个Dispatch Queue。

当生成多个串行Dispatch Queue时,各个串行队列分别对应生成自己的线程,来并行执行,但是每个串行队列同时只能执行一个处理。

如果生成过多的串行队列,就会生成多个线程,因此会消耗大量的内存,引起大量的上下文切换,大幅降低系统的响应性能。

因此只在避免多线程更新相同资源导致的数据竞争问题时,使用Serial Dispatch Queue。因此Serial Dispatch Queue生成的数量仅需所需要的数量。

在不存在数据竞争想并行处理问题时,可以使用Concurrent Dispatch Queue。对于并行队列,不管生成多少,XNU内核只使用有效管理的线程,因此不会出现串行队列大量消耗内存的问题。

因为Dispatch Queue的dispatch_queue_t类型,不是OC对象,不能自动管理其内存,因此通过dispatch_queue_create函数生成的Dispatch Queue需要我们通过dispatch_retain和dispatch_release函数来管理内存。在ARC中不需要使用dispatch_retain和dispatch_release函数。

即使在 dispatch_async后立即通过dispatch_release释放dispatch_queue_create创建的队列,也不会有任何问题。

因为,在dispatch_async函数中追加Block到Dispatch Queue中后,该Block就通过dispatch_retain函数持有了Dispatch Queue,因此,Dispatch Queue不会被立即废弃,Block也可以执行,当Block执行结束后,这时谁都不持有Dispatch Queue,Dispatch Queue被废弃了,因此,会通过dispatch_release函数释放掉Dispatch Queue。

在其他的GCD的API中,名称中含有“create”的API生成的对象,都有必要通过dispatch_retain函数持有,和dispatch_release函数在不需要时释放。在ARC中不需要使用dispatch_retain和dispatch_release函数。

2、获取系统标准提供的Dispatch Queue

通过获取系统提供的Main Dispatch Queue 和Global Dispatch Queue。

Main Dispatch Queue是主线程队列,是一个串行队列。用来执行一些界面更新之类的处理。

Global Dispatch Queue是全局队列,是一个并行队列。Global Dispatch Queue分为四个优先级,分别是:高优先级,默认优先级,低优先级,后台优先级。

系统队列获取的方法:

//Main Dispatch Queue

dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();

//高优先级Global Dispatch Queue

dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

//默认优先级Global Dispatch Queue

dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//低优先级Global Dispatch Queue

dispatch_queue_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

//后台优先级Global Dispatch Queue

dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

对Main Dispatch Queue 和Global Dispatch Queue使用dispatch_retain和dispatch_release函数不会产生任何作用。在ARC中不需要使用dispatch_retain和dispatch_release函数。

dispatch_set_target_queue

1、变更优先级:

使用dispatch_queue_create生成的队列,不管是Serial Dispatch Queue还是Concurrent Dispatch Queue,都是和默认优先级的Global Dispatch Queue相同执行优先级的。

如果需要变更队列的优先级,就需要使用 dispatch_set_target_queue(<#dispatch_object_t _Nonnull object#>, <#dispatch_queue_t _Nullable queue#>)。

其中,第一个参数:是待变更优先级的队列。

第二个参数是:要使用的优先级的Global Dispatch Queue。

但是,待变更的队列不能是Main Dispatch Queue 和Global Dispatch Queue,因为不知道会发生什么状况。

2、变更执行阶层:

并行——>串行:变更执行阶层。

如果多个Serial Dispatch Queue(Queue1,Queue2,Queue3…)都使用dispatch_set_target_queue函数指定目标为一个新的Serial Dispatch Queue(Queue A),那么原先本应并行执行的多个Serial Dispatch Queue(Queue1,Queue2,Queue3…),在目标Serial Dispatch Queue(Queue A)上,就会串行执行,也就是,同时只能执行一个处理。

这种变更执行阶层,在某些情况下可以防止并行处理。

dispatch_after

1、 dispatch_after表示在指定时间后执行处理。但可能不是严格按照指定时间执行,而是稍有延迟。

dispatch_after(<#dispatch_time_t when#>, <#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)

第一个参数:when是指定时间的dispatch_time_t类型;

第二个参数是:要追加处理的队列;

第三个参数是:要处理的Block;

dispatch_time_t类型的时间可以通过dispatch_time或 dispatch_walltime获得。

dispatch_time是通过计算相对时间获取时间;

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 200ull * NSEC_PER_SEC);

表示获取从现在起200毫秒以后的时间。

dispatch_walltime是通过计算绝对时间获取的时间。

dispatch_walltime是从struct timespec类型获取dispatch_time_t时间类型;

Dispatch Group

如果我们想在Dispatch Queue中的多个处理全部执行完成时,做相应的结束处理。

对于Serial Dispatch Queue来说,我们只需要将结束处理追加在队列的最后就可以了。

但是对于Concurrent Dispatch Queue来说,每个任务执行完成的时机是不确定的,因此追加最后的结束处理也比较困难。对于这种情况,我们就可以使用Dispatch Group。

demo如下:

dispatch_group_t group = dispatch_group_create();

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);


    dispatch_group_async(group, queue, ^{NSLog(@"1");});

    dispatch_group_async(group, queue, ^{NSLog(@"2");});

    dispatch_group_async(group, queue, ^{NSLog(@"3");});


    dispatch_group_notify(group, queue, ^{NSLog(@"4");});

同一段代码,多打印几次日志,

日志1
日志2
日志3

可以发现:

前三个Block输出的日志顺序是不确定的,但是最后一个block的输出次序一定是在最后,也就是队列中的前三个Block执行完毕后再执行。

dispatch_group_async的用法与 dispatch_async函数基本相同,都可以追加Block到Dispatch Queue中,与dispatch_async不同的是,dispatch_group_async多了一个参数Dispatch Group,并且指定的Block是属于对应的Dispatch Group的。

在Dispatch Group中也可以使用 dispatch_group_wait函数来在指定时间结束或全部处理结束时执行。

dispatch_group_wait(<#dispatch_group_t _Nonnull group#>, <#dispatch_time_t timeout#>)

中,第一个参数就是要监测的group,第二个参数就是需等待的dispatch_time_t时间类型。

dispatch_group_wait函数一旦调用,就会处于调用而不返回状态,因此,调用其的线程就会处于停止状态,只要等group执行结束或者指定时间到达时,才会返回。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 200ull * NSEC_PER_SEC);

long result = dispatch_group_wait(group, time);

if (result == 0)

{

//返回值result为0时,表示经过指定时间time后,Dispatch Group中的处理依然没有执行结束。

}

else

{

//返回值result不为0时,表示Dispatch Group中的处理已经全部执行结束。

}

返回值result不为0时,表示经过指定时间time后,Dispatch Group中的处理依然没有执行结束。返回值result为0时,表示Dispatch Group中的处理已经全部执行结束。

dispatch_barrier_async

dispatch_barrier_async函数结合 dispatch_queue_create函数生成的Concurrent Dispatch Queue一起,可以使追加在dispatch_barrier_async之前的处理全部执行结束后,再将dispatch_barrier_async中追加在Queue里的处理执行。dispatch_barrier_async中的处理执行完毕后,再恢复dispatch_async的一般动作,执行dispatch_barrier_async之后所追加的处理。

dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, block1);

dispatch_async(queue, block2);

dispatch_async(queue, block3);

dispatch_barrier_async(queue, block4);

dispatch_async(queue, block5);

dispatch_async(queue, block6);

dispatch_async(queue, block7);

使用dispatch_barrier_async函数和 dispatch_queue_create函数生成的Concurrent Dispatch Queue可以实现高效率的安全的数据库访问和文件访问。

dispatch_sync

dispatch_sync是等待处理执行结束后,再继续。

一旦调用,在指定处理结束之前,该函数不会返回。效果等同于dispatch_group_wait函数。

dispatch_sync和主线程队列Main Dispatch Queue,以及Serial Dispatch Queue一起使用时,很容易形成死锁。需谨慎使用。

比如,下面的和Main Dispatch Queue一起使用:

dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(queue, ^{

        dispatch_sync(queue, ^{NSLog(@"hello");});

    });

    NSLog(@"what");

在执行这几行代码时,会在第三行代码处,报Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)死锁的错误。因此最后一行代码时执行不到的。

同样的,在和Serial Dispatch Queue一起使用时,也容易发生类似问题:

dispatch_queue_t queue = dispatch_queue_create("queue", NULL);

    dispatch_async(queue, ^{

        dispatch_sync(queue, ^{NSLog(@"hello");});

    });

    NSLog(@"what");

在执行这几行代码时,会在第三行代码处,报Thread 4: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)死锁的错误。因此最后一行代码时执行不到的。

dispatch_apply

dispatch_apply(<#size_t iterations#>, <#dispatch_queue_t  _Nonnull queue#>, <#^(size_t)block#>)

第一个参数iterations:重复的次数;

第二个参数queue:要追加的队列;

第三个参数block:要追加的处理;

demo如下:

NSArray *array = [NSArray arrayWithObjects:@0, @1, @2, @3, nil];

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply([array count], queue, ^(size_t index){

    NSLog(@"%@",[array objectAtIndex:index]);

});

根据打印的日志

dispatch_apply日志

可以看出, dispatch_apply是非同步的。但是 dispatch_apply会等待追加的处理全部执行完毕。

挂起指定队列:

dispatch_suspend(queue);

恢复指定队列:

dispatch_resume(queue);

dispatch_semaphore_wait

dispatch_semaphore_wait(<#dispatch_semaphore_t  _Nonnull dsema#>, <#dispatch_time_t timeout#>)

是比Serial Dispatch Queue和dispatch_barrier_async更细粒度的避免数据竞争的排他控制方法。

dispatch_semaphore_wait和dispatch_group_wait一样,可以设定等待时间,当指定时间到达时,或者Dispatch Semaphore的计数值大于等于1时返回。返回时会将Dispatch Semaphore的计数值减1。

当Dispatch Semaphore的计数值大于等于1时或者在指定等待时间内Dispatch Semaphore的计数值大于等于1时,返回值为0。当指定时间到时,Dispatch Semaphore的计数值为0的话,返回值不为0。

当dispatch_semaphore_wait返回值为0时,就可安全的执行排他处理。处理结束后,使用 dispatch_semaphore_signal函数将Dispatch Semaphore的计数值加1。

dispatch_once

dispatch_once函数保证程序中只执行一次处理的函数。通常用在生成单例对象时。

总结

使用GCD要比使用NSThread和pthreads这些一般的多线程编程API更好。因为任何由程序员编写的管理线程的代码,在性能方面都比不上iOS和OS X的核心XNU内核级实现的GCD。因此我们可以尽量多使用GCD或者使用Cocoa框架中GCD的NSOperation类的API。

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

推荐阅读更多精彩内容