GCD总结笔记

章节目录

  • 什么是GCD?
  • 如何在多条路径中执行CPU命令列?
  • 即使多线程存在很多问题(如数据竞争、死锁、线程过多消耗大量内存),为何还要使用多线程?
  • Dispatch Queue
  • dispatch_set_target_queue
  • dispatch_after
  • dispatch_time
  • dispatch_walltime
  • Dispatch Group
  • dispatch_group_wait
  • dispatch_barrier_async
  • dispatch_sync 与 dispatch_async
  • dispatch_apply
  • dispatch_suspend / dispatch_resume
  • Dispatch Semaphore
  • dispatch_once
  • Dispatch I/0
  • GCD实现
  • 参考资料

正文:

  • 什么是GCD?

GCD是异步执行任务的技术之一。
一般应用程序中记述的线程管理用的代码在系统中实现。
所以我们只需定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。

在导入GCD之前,Cocoa框架提供了NSThread等简单的多线程编程技术,但通过源码对比可知GCD更为简洁,执行效率也更高。

一个CPU一次只能执行一个命令,不能执行某处分开的并列的两个命令,因此通过CPU执行的CPU命令列就好比一条无分叉的大道,其执行不会出现分歧。
而在OC的if等控制语句或函数调用的情况下,执行命令列的地址会远离当前的位置(位置迁移)

一个CPU执行的CPU命令列为一条无分叉路径,即为线程。
多线程即为多条路径。多线程中,1个CPU核执行多条不同路径上的不同命令。
一个64核的CPU芯片有64个CPU。


CPU命令列.png
  • 一个CPU核一次能够执行的CPU命令始终为1,那如何在多条路径中执行CPU命令列?

OSX和iOS的核心XNU内核在发生操作系统事件时会切换执行路径(如每隔一定时间,唤起系统调用等情况)。
执行中路径的状态,如CPU的寄存器等信息保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令列。这被称为”上下文切换”。

由于使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去就好像一个CPU核能够并列地执行多个线程一样。

但具有多个CPU核时,就不是看上去像了,而是真的多个CPU核并行执行多个线程的技术。

这种利用多线程编程的技术就被称为多线程编程。

  • 即使多线程存在很多问题(如数据竞争、死锁、线程过多消耗大量内存),为何还要使用多线程?

多线程编程可保证应用程序的响应性能。
主线程是应用程序启动时最先执行的线程,负责描绘用户界面、处理触摸屏幕的事件等。
若在主线程进行长时间的处理,就会妨碍主线程的执行,从而导致不能应用程序的画面没有更新而长时间停滞等问题。

Dispatch Queue : 执行处理的等待队列。根据FIFO执行操作的处理。
在执行处理时存在两种Dispatch Queue,一种是等待现在执行中的处理的Serial Dispatch Queue(同步),另一种是不等待现在执行中处理的Concurrent Dispatch Queue(异步)。

Dispatch Queue种类.png


并行执行,使用多个线程同时执行多个处理。
但是并行执行的处理数量取决于当前系统的状态(Concurrent Dispatch Queue)。
iOS和OS X的核心 —— XNU内核决定应当使用的线程数,并只生成所需的线程执行处理。
当处理结束,应当执行的处理数减少时,XNU内核会结束不再需要的线程(中间会有个放入线程缓存池的操作)。

  • 如何才能得到Dispatch Queue?

两种方法。

第一种

通过CGD的API: dispatch_queue_create函数生成Dispatch Queue
一个Serial Dispatch Queue 同时只能执行一个追加处理,但使用 dispatch_queue_create函数可生成任意多个Dispatch Queue。

当生成多个Serial Dispatch Queue时,各个Serial Dispatch Queue将并行执行。虽然一个队列只能生成一个处理,但可以有多个队列同时进行一个处理,即为同时执行多个处理。
但不可生成太多,否则会消耗大量内存,引起大量的上下文切换,大幅降低系统的响应性能。

dispatch_queue_create函数,第一个参数指定Serial Dispatch Queue的名称。
第二个参数若为NULL则生成的是Serial Dispatch Queue,若为DISPATCH_QUEUE_CONCURRENT则生成的是Concurrent Dispatch Queue。

生成的Dispatch Queue必须由程序员负责释放,通过dispatch_release(队列名)释放。可
通过dispatch_retain(队列名)增加引用。
即Dispatch Queue也像OC的引用计数式内存管理一样,需通过dispatch_retain函数和dispatch_release函数的引用计数来管理内存。

GCD的使用.png

如图所示,在dispatch_async函数中追加Block到Concurrent Dispatch Queue,并立即通过dispatch_release函数进行释放。则该Block通过dispatch_retain函数持有Dispatch Queue。无论Serial Dispatch Queue还是Concurrent Dispatch Queue都一样。

于是在dispatch_async函数中追加Block到Dispatch Queue后,即使立即释放Dispatch Queue,该Dispatch Queue由于被Block所持有也不会被废弃,因而Block能够执行。Block执行结束后会释放Dispatch Queue,此时谁都不持有Dispatch Queue,它也因此会被废弃。

第二种

获取系统标准提供的Dispatch Queue(Main Dispatch Queue 和 Global Dispatch Queue)

Main Dispatch Queue是在主线程中执行的Dispatch Queue。因主线程只有一个,所以其自然是Serial Dispatch Queue。
追加到Main Dispatch Queue的处理在主线程的RunLoop中执行。所以将用户界面的界面更新等一些必须在主线程中执行的处理追加到Main Dispatch Queue使用。

Global Dispatch Queue 是所有应用程序都能够使用的Concurrent Dispatch Queue。
其有四个执行优先级,分别是高、默认、低、后台。
通过XNU内核管理的用于Global Dispatch Queue的线程,将各自使用的Global Dispatch Queue的执行优先级作为线程的优先级使用。
所以在使用时,应注意优先级的选择,但通过XNU内核用于Global Dispatch Queue的线程并不能保证实时性,因此执行优先级只是大致的判断。
列如在处理内容的执行可有可无时,使用后台优先级的这种。

后台优先级.png
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_HEIGH,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函数不会引起任何变化,也不会有任何问题。
这也是使用其会更轻松的原因。

dispatch_get_global_queue的第二个参数,官方解释为留待未来使用,非0就可能返回NULL

  • dispatch_set_target_queue

dispatch_queue_create函数生成的Dispatch Queue 不管是Serial Dispatch Queue 还是 Concurrent Dispatch Queue,其使用的线程优先级都与Global Dispatch Queue的优先级相同。而变更优先级就使用dispatch_set_target_queue函数。

代码演示:
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create(“DrunkenMouse”,NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(mySerialDispatchQueue,globalDispatchQueueBackground);

第一个参数的Dispatch Queue的优先级会修改为与第二个Dispatch Queue的优先级相同。
但第一个参数若为Main Dispatch Queue 和 Global Dispatch Queue则不知道会出现什么状况,所以不可指定。

  • dispatch_after

指定多少时间后追加操作到Dispatch Queue,而不是指定时间后执行处理。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW,3ull * NSEC_PER_SEC);
dispatch_after(time,dispatch_get_main_queue(),^{
    NSLog(@“wait at least 3 seconds”);
});

此源码意为3秒后追加Block操作到Main Dispatch Queue。

  • dispatch_time

dispatch_time函数获取从第一个参数指定的时间开始到第二个参数指定的毫微妙单位时间后的时间。
第一个参数是指定时间用的dispatch_time_t类型的值,使用dispatch_time函数或dispatch_walltime函数生成
第二个参数中ull * NSEC_PER_SEC代表秒,使用NSEC_PER_MSEC代表毫秒
如150毫秒:150ull * NSEC_PER_MESC
ull是C语言的数值字面量,表示unsigned long long

  • dispatch_walltime

dispatch_walltime函数用于计算绝对时间。比如想指定在X年X月X日X时X分X秒这一绝对时间。

  • Dispatch Group

用于将多个Dispatch Queue 添加到同一个Dispatch Group, 待Dispatch Queue全部执行完毕后执行某项操作。(Serial Dispatch Queue或 Concurrent Dispatch Queue皆可)

dispatch_group_t group = dispatch_group_create(); 生成group
dispatch_group_async(group, queue, ^{NSLog(@“DrunkenMouse”);}); 将操作添加到队列queue,将队列queue添加到组group中
dispatch_group_async(group, queue, ^{NSLog(@“DrunkenMouse2”);}); 组中可添加多个queue
dispatch_group_notify(group,dispatch_get_main_queue(),^{NSLog(@“done”);}); 通过notify监听group,在group中所有操作执行完毕后,将第三个参数的Block添加到第二个参数的Dispatch Queue。
dispatch_release(group);释放组

dispatch_group_create函数生成dispatch_group_t类型的Dispatch Group。
该Dispatch Group与Dispatch Queue相同,在使用结束后通过dispatch_release函数释放。
dispatch_group_async函数将Block追加到指定的Dispatch Queue,Block属于指定的Dispatch Group。
所以Block通过dispatch_retain函数持有Dispatch Group,于是Block执行结束,该Block就通过dispatch_release函数释放持有的Dispatch Group。
一旦Dispatch Group使用结束,不用考虑Block,立即通过Dispatch_release函数释放即可。

  • dispatch_group_wait

在Dispatch Group中使用dispatch_group_wait函数仅等待全部处理执行结束。
dispatch_group_t group = dispatch_group_create(); 生成group
dispatch_group_async(group, queue, ^{NSLog(@“DrunkenMouse”);}); 将操作添加到队列queue,将队列queue添加到组group中
dispatch_group_async(group, queue, ^{NSLog(@“DrunkenMouse2”);}); 组中可添加多个queue
dispatch_group_wait(group,DISPATCH_TIME_FOREVER);
第二个参数为等待的时间,属于dispatch_time_t类型的值。DISPATCH_TIME_FOREVER意味永久等待,只要group中的处理尚未结束就一直等待。
等待的时间也可以为1秒

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW,1ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group,time);
if(result == 0) {
    //属于Dispatch Group的全部处理执行结束
}else {
    //属于Dispatch Group的某一个处理还在执行中
}

等待意味着一旦调用dispatch_group_wait函数,该函数就处于调用的状态而不返回。
在经过指定时间或Dispatch Group的处理全部执行结束之前,执行该函数的线程停止。
若不等待则可以使用DISPATCH_TIME_NOW:

long result = dispatch_group_wait(group,DISPATCH_TIME_NOW);
  • dispatch_barrier_async

dispatch_barrier_async 栅栏,可保证写入时不会读取,读取时不会写入。
进而可将写入操作放到Serial Dispatch Queue中,读取操作放入Serial Dispatch Queue,以提高性能。

dispatch_async(queue,blk1_reading);
dispatch_async(queue,blk2_reading);
dispatch_barrier_async(queue,blk3_writing);
dispatch_async(queue,blk4_reading);

dispatch_barrier_async函数会等到目前为止的并行都处理结束后,再去执行此操作,而在执行该写入操作时禁止再执行别的操作。

  • dispatch_sync 与 dispatch_async

dispatch_async 异步,将指定的Block”非同步”的追加到指定的Dispatch Queue,dispatch_async函数不做任何等待。
dispatch_sync 同步,将指定的Block”同步”追加到指定的Dispatch Queue中,在追加Block结束之前,dispatch_sync函数会一直等待。
等待的意思同dispatch_group_wait,该线程直到处理结束才会返回。
同步主队列操作会导致死锁。也就是dispatch_sync(dispatch_get_main_queue(),blk1);
死锁原因:Main Dispatch Queue会等待主线程中操作执行执行完毕再执行该操作。于是主线程等待该操作结束才继续执行,该操作又在等待主线程操作结束才能执行。

同一个队列,异步操作里嵌套同步操作会发生Crash。(XCode8 测试)

  • dispatch_apply

该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束。
第二个参数为重复次数,第二个参数为添加到的队列,第三个参数为追加的处理

dispatch_apply(10,queue,blk1);
NSLog(@“DrunkenMouse”);

blk1添加10此到队列queue,待操作全都执行完毕后输出DrunkenMouse
第三个参数Block为带有参数的Block,这是为了区分添加的Block所用。
由于dispatch_apply函数与dispatch_sync函数相同,会等待处理执行结束。
因此推荐在dispatch_async函数非同步的执行dispatch_apply函数。

  • dispatch_suspend / dispatch_resume

dispatch_suspend(queue)将指定的Dispatch Queue挂起。
即不执行追加到该Dispatch Queue中未执行的处理,但已经执行的处理不受影响。
dispatch_resume(queue)恢复指定的Dispatch Queue

  • Dispatch Semaphore

数据信号灯。当计数为0时等待,计数为1或大于1时,减去1而不等待。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
参数表示计数的初始值,由create可看出,同样需dispatch_release函数释放。可通过dispatch_retain函数持有。
dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
该函数等待Dispatch Semaphore的计数值大于或等于1。当满足时计数减一并从dispatch_semaphore_wait函数返回。
返回0代表计数值大于或等于1,计数减一。否则就是没返回值。
第二个参数与dispatch_group_wait函数等相同,由dispatch_time_t类型值指定等待时间。
计数值的加1通过dispatch_semaphore_signal函数

  • dispatch_once

一次执行。在程序运行阶段,其函数只会执行一次。

  • Dispatch I/0

在读取较大文件时,可将文件分成合适的大小并使用Global Dispatch Queue并列读取来提升速度。实现这一功能的就是Dispatch I/O 和 Dispatch Data

  • GCD实现

编程人员所使用GCD的API全部为包含在libdispatch 库中的C语言函数。
Dispatch Queue通过结构体和链表,被实现为FIFO队列。
FIFO队列管理是通过dispatch_async等函数所追加的Block。
Block并不是直接加入FIFO队列,而是先加入Dispatch Continuation这一dispatch_continuation_t类型结构体中,然后再加入FIFO队列。
该DispatchContinuation用于记忆Block所属的DispatchGroup和其他一些信息,相当于一般常说的执行上下文。

Global Dispatch Queue有8种:
HighPriority DefaultPriority LowPriority BackgroundPriority
HighOvercommitPriority DefaultOvercommitPriority LowOvercommitPriority BackgroundOvercommitPriority
优先级中附有Overcommit的Global Dispatch Queue使用在SerialDispatchQueue中。
如Overcommit这个名称所示,不管系统状态如何,都会强制生成线程的DispatchQueue
这八种Global Dispatch Queue各使用一个pthread_workqueue.
GCD初始化时,使用pthread_workqueue_create_np函数生成pthread_workqueue.
pthread_workqueue包含在Libc提供的Pthreads API中。其使用bsdthread_register和workq_open系统调用,在初始化XNU内核的work queue之后获取workqueue信息
XNU内核持有4种work queue
WORKQUEUE_HIGH_PRIOQUEUE
WORKQUEUE_DEFAULT_PRIOQUEUE
WORKQUEUE_LOW_PRIOQUEUE
WORKQUEUE_BG_PRIOQUEUE

Dispatch Queue中执行Block的过程。当在Global Dispatch Queue 中执行Block时,libdispatch从Global Dispatch Queue自身的FIFO队列中取出Dispatch Continuation,调用pthread_workqueue_additem_np函数。将该Global Dispatch Queue自身、符合其优先级的work queue信息以及为执行Dispatch Continuation的回调函数等传递给参数。

看图.png


pthread_workqueue_additem_np函数使用workq_kernreturn系统调用,通知work queue增加应当执行的项目。根据该通知,XNU内核基于系统状态判断是否要生成线程,如果是Overcommit优先级的Global Dispatch Queue,workqueue则始终生成线程。
work queue的线程执行pthread_workqueue函数,该函数调用libdispatch的回调函数。在该回调函数中执行加入到DispatchContinuation的Block
Block执行结束后,进行通知DispatchGroup结束、释放DispatchContinuation等处理,开始准备执行加入到GlobalDispatchQueue中的下一个Block。

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

推荐阅读更多精彩内容