iOS与OS X多线程和内存管理(三)Grand Central
前言
该篇内容是《iOS与OS X多线程和内存管理》一书中的最后内容了,GCD的学习除了书本的学习,我还结合了一些优秀的第三方类库的源码进行学习,可能学习的不够透彻但已经是收益匪浅,以下内容如有理解错误的地方,希望大家指出,我们一起学习和探讨~
Grand Central Dispatch(GCD)概要
1、什么是GCD
- 苹果官方说明:Grand Central Dispatch(GCD)是异步执行任务的技术之一,一般将应用程序中记述的线程管理用的代码在系统级中实现,开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务,由于线程管理是作为系统一部分来实现的,由此可统一管理,也可执行任务,这样就可以比以前的线程更有效率
2、多线程编程
- 源代码实际在Mac或iphone上是如何执行的?
int main(){
id o = [[MyObject alloc] init];
[o execBlock];
return o;
}
160D25D4D00934E5CA9CECEE5626BE27.jpg
-
答案:
- 虽然调用了几个方法,但代码行基本是按从上到下的顺序执行的
- 汇集CPU命令列和数据,将其作为一个应用程序安装在Mac或iphone上
- Mac、iphone的操作系统OS X、ios根据用户的指示启动该应用程序后,首先便将包含在应用程序中的CPU命令列配置在内存中
- CPU从应用程序指定的地址开始,一个一个的执行CUP命令列(先执行地址lac的命令列push,接着向后移,执行地址lae的命令列movw,再次向后移动,执行地址lb2的命令列,这样不断循环下去)
-
在OC的if语句和for语句等控制语句或函数调用的情况下,执行命令列的地址会远离当前的位置(位置迁移),但是由于一个CUP一次只能执行一个命令,不能执行某处分开的并列的两个命令,因此通过CPU执行的CPU命令列就好比一条无分叉的大道,其执行不会出现分歧
- 这里所说的“一个CPU执行的CPU命令列尾一条无分叉的路径”即为“线程”
- 一个物理的CPU芯片实际上有64个(64核)CPU,如果一个CUP核虚拟出来两个CPU核工作,那么一台计算机上使用多个CPU核就是理所当然的事了,尽管如此“一个CPU核执行的CPU命令列为一条无分叉路经”仍然不变
- 这种无分叉路经不只1条,存在多条时即“多线程”,在多线程中1个CPU核执行多条不同路经上的不同命令
-
基本上一个CPU核一次能够执行的CPU命令列始终是1,那么怎么才能在多条路经中执行CPU命令列?
- OS X和ios的核心XNU内核在发生操作系统事件时(如每隔一定时间,唤起系统调用等情况)会切换执行路经。执行中路经的状态,例如CPU的寄存器的信息保存到各自路经专用的内存块中,从切换目标路经专用的内存块中,复原CPU寄存器的信息,继续执行切换路经的CPU命令列,这被称为“上下文切换”
-
由于使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看起来就像一个CPU核能够并列的执行多个线程一样,并且在具有多个CPU核的情况下就更像
-
多线程易发生的问题
- 应用程序启动时。通过最先执行的线程,即‘主线程’来描绘用户界面、处理触摸屏幕的事件,如果在该线程中进行长时间的处理,会造成主线程阻塞,会妨碍主线程中被称为RunLoop的主循环执行,从而导致不能更新用户界面、应用程序画面长时间停滞等问题
GCD的API
1、Dispatch Queue
- 开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中
/*
* 使用Block语法‘定义想执行的任务’
* 通过dispatch_async函数‘追加’赋值在变量queue的Dispatch Queue中
* 该代码就可使指定的Block在另一个线程中执行
*/
dispatch_async(queue, ^{
/*
*想执行的任务
*/
});
- Dispatch Queue:执行处理的等待队列
- 编程人员通过dispatch_async函数等API,在Block语法中记述想执行的处理并将其追加到Dispatch Queue中
- Dispatch Queue按照追加的顺序(先进先出FIFO)执行处理
- 在执行处理时存在两种Dispatch Queue
- 一种是等待现在执行中处理的 Serial Dispatch Queue
- 一种是不等待现在执行中处理的Concurrent Dispatch Queue
- Serial Dispatch Queue
/*
* 当变量queue为Serial Dispatch Queue时,因为等待现在执行中的处理结束,所以首先执行blk0,blk0 执行结束后接着执行blk1,blk1 执行结束后接着执行blk2,如此重复
* 同时执行的处理数只能时1个
*/
dispatch_async(queue , blk0);
dispatch_async(queue , blk1);
dispatch_async(queue , blk2);
dispatch_async(queue , blk3);
dispatch_async(queue , blk4);
dispatch_async(queue , blk5);
dispatch_async(queue , blk6);
dispatch_async(queue , blk7);
//执行顺序
blk0
blk1
blk2
blk3
blk4
blk5
blk6
blk7
- Concurrent Dispatch Queue
- 当变量queue为Concurrent Dispatch Queue时,因为不等待现在执行中的处理结束,所以首先执行blk0,不管blk0的执行是否结束,都开始执行后面的blk1,不管blk1的执行是否结束,都开始执行blk2,如此重复
- 虽然不用等待处理结束,可以并行执行多个处理,但并行执行的处理数量取决于当前系统的状态,即ios 和OS X基于Dispatch Queue中的处理数,CPU核数以及CPU负荷等当前系统的状态来决定Concurrent Dispatch Queue中并行执行的处理数(所谓的‘并行执行’就是使用多个线程同时执行多个处理)
dispatch_async(queue , blk0);
dispatch_async(queue , blk1);
dispatch_async(queue , blk2);
dispatch_async(queue , blk3);
dispatch_async(queue , blk4);
dispatch_async(queue , blk5);
dispatch_async(queue , blk6);
dispatch_async(queue , blk7);
- XNU内核决定应当使用的线程数,并只生成所需的线程执行处理,另外,当处理结束,应当执行的处理数减少时,XNU内核会结束不再需要的线程,XNU内核仅使用Concurrent Dispatch Queue便可完美的管理并行执行处理的线程
-
假设准备4个Concurrent Dispatch Queue用线程,首先blk0在线程0中开始执行,接着blk1在线程1中,blk2在线程2中,blk3在线程3中执行,线程0中blk0执行结束后开始执行blk4,由于线程1中的blk1还没执行结束,线程2中的blk2执行结束,因此在线程2中执行blk5,就此循环往复
-
生成Dispatch Queue的方式
- 通过GCD的API生成Dispatch Queue
- 获取系统标准提供的Dispatch Queue
2、dispatch_queue_create
- 通过GCD的API生成Dispatch Queue(dispatch_queue_create函数)
/*
* 第一个参数:指定Dispatch Queue名称(推荐使用应用程序ID这种逆序全程域名,该名称便与调试)
* 第二个参数:Serial Dispatch Queue指定为NULL;Concurrent Dispatch Queue指定为DISPATCH_QUEUE_CONCURRENT
* dispatch_queue_create函数的返回值为表示Dispatch Queue的‘ dispatch_queue_t类型’
* dispatch_queue_create函数生成的Dispatch Queue必须程序员自己负责释放
* Dispatch Queue也像OC的引用计数式内存管理一样,需要通过dispatch_retain函数和dispatch_release函数的引用计数来管理内存
* 在dispatch_async函数中追加Block到Dispatch Queue(该Block通过dispatch_retain函数持有Dispatch Queue(无论Serial Dispatch Queue、Concurrent Dispatch Queue都一样))
* 一旦Block执行结束,就要通过dispatch_release函数函数释放该Block持有的Dispatch Queue
* 在dispatch_async函数中追加Block到Dispatch Queue后,即是立刻释放Dispatch Queue,该Dispatch Queue由于被Block持有也不会废弃,因而Block能够执行,Block执行结束后释放该Block持有的Dispatch Queue,这时谁都不持有Dispatch Queue,因此它被废弃
*/
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create ("com.example.MySerialDispatchQueue" , NULL);
dispatch_async(queue, ^{
});
dispatch_release(mySerialDispatchQueue)
- Concurrent Dispatch Queue并行执行多个追加的处理,Serial Dispatch Queue同时只能执行一个追加处理,虽然Concurrent Dispatch Queue、Serial Dispatch Queue受系统资源的限制,但是使用dispatch_queue_create函数可生成任意多个Dispatch Queue
- 当生成多个Serial Dispatch Queue时,各个Serial Dispatch Queue将并执行,虽然一个Serial Dispatch Queue中同时只能执行一个追加处理,但如果将处理分别追加到4个Serial Dispatch Queue中,各个Serial Dispatch Queue执行1个,即为同时执行4个处理
- 一旦生成Serial Dispatch Queue并追加处理,系统对于一个Serial Dispatch Queue就只生成并使用一个线程,如上就生成了4条线程,如果生成的线程过多,就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能
- Serial Dispatch Queue的生成个数应当仅限所必须的数量
- 在通过函数或方法名获取Dispatch Queue以及其他名称中包含‘create’的API生成的对象时,有必要通过dispatch_retain函数持有,并在不需要时通过dispatch_release函数释放
3、Main Dispatch Queue / Global Dispatch Queue
- Main Dispatch Queue是在主线程中执行的Dispatch Queue,因为主线程只有一个,所以Main Dispatch Queue自然就是Serial Dispatch Queue
- 追加到Main Dispatch Queue中的处理在主线程的RunLoop中执行
- Global Dispatch Queue是所有应用程序都能够使用的Concurrent Dispatch Queue,没有必要通过dispatch_queue_create函数逐个生成Concurrent Dispatch Queue,只要获取Global Dispatch Queue使用即可
- Global Dispatch Queue有4个执行优先级
- 最高优先级(High Priority)
- 默认优先级(Default Priority)
- 低优先级(Low Priority)
- 后台优先级(Background Priority)
- 同XNU内核用于Global Dispatch Queue的线程并不能保证实时性,所以优先级只是个大致判断
- 各种Dispatch Queue的获取方法:
- 对于Main Dispatch Queue和Global Dispatch Queue执行dispatch_retain函数和dispatch_release函数不会引起任何变化,也不会有任何问题,这也是获取并使用Global Dispatch Queue比生成、使用、释放Concurrent Dispatch Queue更轻松的原因
4、dispatch_set_target_queue
- dispatch_queue_create函数生成的Dispatch Queue不管是Serial Dispatch Queue还是Concurrent Dispatch Queue,都是用默认优先级的Global Dispatch Queue相同执行优先级的线程,而变更生成的Dispatch Queue的执行优先级要使用dispatch_set_target_queue函数
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue",NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_PRIORITY_BACKGROUND ,0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);
/*
* 指定要变更执行优先级的Dispatch Queue为dispatch_set_target_queue函数的第一个参数
* 指定与要使用的执行优先级相同优先级的Dispatch Queue为第二个参数(目标)
* Main Dispatch Queue和Global Dispatch Queue不可指定为第一个参数
*/
- 将Dispatch Queue指定为dispatch_set_target_queue函数的参数,不仅可以变更Dispatch Queue的执行优先级,还可以作成Dispatch Queue的执行阶层
- 多个Serial Dispatch Queue中用dispatch_set_target_queue函数指定目标为某一个Serial Dispatch Queue,那么原先本应并行执行的多个Serial Dispatch Queue,在目标Serial Dispatch Queue上只能同时执行一个处理(可防止Serial Dispatch Queue处理并行执行)
5、dispatch_after
- 在指定时间后执行处理
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW , 3ull * NSEC_PER_SEC);
dispatch_after(time , dispatch_get_main_queue(), ^{
NSLog(@"waited at least three seconds ");
});
/*
* 注意:dispatch_after函数并不是在指定时间后执行处理,而只是在指定时间追加处理到Dispatch Queue,上述代码与3秒后用dispatch_async函数追加Block到Main Dispatch Queue的相同
* 因为Main Dispatch Queue在主线程的RunLoop中执行,所以在比如每隔1/60秒执行的RunLoop中,Block最快在3秒后执行,最慢在3秒+1/60秒后执行,并且在Main Dispatch Queue有大量处理追加或主线程的处理本身有延迟是,这个时间回更长
* 第二个参数:指定要追加处理的Dispatch Queue
* 第三个参数:指定记述要执行处理的Block
* 第一个参数:指定时间用的dispatch_time_t类型的值
*/
- dispatch_time_t类型的值使用dispatch_time函数或者dispatch _walltime函数作成
-
dispatch_time函数能够获取从第一个参数dispatch _time_t类型值中指定的时间开始,到第二个参数指定的时间后的时间
- 第一个参数:DISPATCH_TIME_NOW 现在的时间
- 第二个参数:数值和NSEC_PER_SEC的乘积得到单位为毫微秒的数值,ull是C语言的数值字面量,是显示表明类型时使用的字符串(表示‘unsigned long long’) ,NSEC_PER_MSEC表示毫秒单位
-
dispatch_walltime函数由POSLX中使用的struct timespec类型的时间得到dispatch _time_t类型的值
-
dispatch _time函数通常用于计算相对时间
-
dispatch_walltime函数用于计算绝对时间,例如dispatch_after函数中想指定2018年11月11日11时11分11秒这一绝对时间,类似闹钟
-
6、Dispatch Group
- 使用Dispatch Group实现:追加3个Block到Global Dispatch Queue ,在这些Block都执行完毕,就会执行Main Dispatch Queue 中结束处理中用的Block
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ NSLog(@"blk0");});
dispatch_group_async(group, queue, ^{ NSLog(@"blk1");});
dispatch_group_async(group, queue, ^{ NSLog(@"blk2");});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done");});
dispatch_release(group);
//执行结果
blk1
blk2
blk0
done
- 因为向Global Dispatch Queue即Concurrent Dispatch Queue追加处理,多线程并行执行,所以追加处理的执行顺序不定,执行时会发生变化,但在此执行结果中done一定是最后输出的
- Dispatch Group可监视这些处理执行的结束。一旦检测到所有的处理执行结束,就可将结束的处理追加到Dispatch Queue中
- 使用dispatch_group_create函数生成dispatch_group_t类型的Dispatch Group,因为函数名中含有create,所以在使用结束后需要通过dispatch_release函数释放
- dispatch_group_async函数追加Block到指定的Dispatch Queue中,与dispatch_async
函数不同的是指定生成的Dispatch Group为第一个参数,指定的Block属于指定的Dispatch Group - 与追加Block到Dispatch Queue时同样,Block通过dispatch_retain函数持有Dispatch Group,从而使得该Block属于Dispatch Group,这样如果Block执行结束,该Block就通过dispatch_release函数释放持有的Dispatch Group,一旦Dispatch Group使用结束,不用考虑属于该Dispatch Group的Block,立即通过dispatch_release函数释放即可
- 在追加到Dispatch Group中的处理全部执行结束时,dispatch_group_notify函数会将执行的Block追加到Dispatch Queue中,将第一个参数指定为要监视的Dispatch Group。在追加到该Dispatch Group中的全部处理执行结束时,将第三个参数的Block追加到第二个参数的Dispatch Queue中,在dispatch_group_notify函数中不管指定什么样的Dispatch Queue,属于Dispatch Group的全部处理在追加指定的Block时都已执行结束
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ NSLog(@"blk0");});
dispatch_group_async(group, queue, ^{ NSLog(@"blk1");});
dispatch_group_async(group, queue, ^{ NSLog(@"blk2");});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
-
在Dispatch Group中使用dispatch_group_wait函数仅等待全部执行结束
-
dispatch_group_wait函数第二个参数指定为等待时间(超时)dispatch_time_t类型的值,DISPATCH_TIME_FOREVER永久等待,只要属于Dispatch Group的处理尚未执行结束,,就会一直等待,中途不会取消
-
dispatch_group_wait函数
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的全部处理执行结束
}eles{
//属于Dispatch Group的某一个处理还在执行
}
- 如果dispatch_group_wait函数的返回值不为0,就意味着虽然经过了指定的时间,但属于Dispatch Group的某一个处理还在执行中,如果返回值为0,那么全部处理执行结束,当等待时间为DISPATCH_TIME_FOREVER永久等待,由dispatch_group_wait函数返回时,由于属于Dispatch Group的处理必定全部执行结束,因此返回值恒为0
- 一旦调用dispatch_group_wait函数,该函数就处于调用的状态而不返回,即执行dispatch_group_wait函数的现在线程(当前线程)停止,在经过dispatch_group_wait函数中指定的时间或属于指定Dispatch Group的的处理全部执行结束之前,执行该函数的线程停止
- 指定DISPATCH_TIME_NOW,则不用任何等待即可判定属于Dispatch Group的处理是否执行结束
7、dispatch_barrier_async
- dispatch_barrier_async 函数同dispatch_queue_create函数生成的Concurrent Dispatch Queue一起使用
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.Forbarrier",DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_barrier_async(queue, blk_for_writing);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);
dispatch_async(queue, blk7_for_reading);
dispatch_release(queue);
4660EEE669D58074804C874EA0C78C1D.jpg
- 使用dispatch_barrier_async函数会等待追加到Concurrent Dispatch Queue上的并行执行的处理全部结束后,在将指定的处理追加到该Concurrent Dispatch Queue中,然后在有dispatch_barrier_async函数追加的处理执行完毕后,Concurrent Dispatch Queue才恢复一般动作,追加到Concurrent Dispatch Queue的处理又开始并行执行
- dispatch_barrier_async 函数同dispatch_queue_create函数生成的Concurrent Dispatch Queue一起使用可以实现高效率的数据库访问和文件访问
8、dispatch_sync
- dispatch_async函数,‘非同步’
- dispatch_sync函数,‘同步’
- 一旦调用dispatch_sync函数,那么在指定的处理执行结束之前,该函数不会返回
- dispatch_sync函数易出现的问题:死锁
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{NSLog(@"Hello");});
//Main Dispatch Queue即主线程中执行的Block等待Main Dispatch Queue中执行的Block执行结束(形成死锁)
9、dispatch_apply
- dispatch_apply函数是,dispatch_sync函数和Dispatch Group的关联API,该函数按指定的次数将指定的Block追加到指定的Dispatch Queue 中,并等待全部处理执行结束
/*
* 第一个参数:重复次数
* 第二个参数:追加对象的Dispatch Queue
* 第三个参数:为追加的处理
*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRORITY_DEFAULT,0);
dispatch_apply(10, queue, ^(size_t index){
NSLog(@"%zu",index);
});
NSLog(@"done");
//执行结果
4
1
0
3
5
2
6
8
9
7
done
- 因为在Global Dispatch Queue中执行,所以各个处理的执行时间不定,但是输出结果的最后必定是done,这是因为dispatch_apply函数或等待全部处理执行结束
- 因为dispatch_apply和dispatch_sync函数一样,会等待处理执行结束,因此推荐在dispatch_async函数中非同步的执行dispatch_apply函数
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRORITY_DEFAULT,0);
//在Global Dispatch Queue中非同步执行
dispatch_async(queue, ^{
//Global Dispatch Queue,等待dispatch_apply函数中全部处理执行结束
dispatch_apply([array count], queue, ^(size_t index){
//并列处理包含在NSArray对象的全部对象
NSLog(@"%zu :%@",index,[array objectAtIndex:index]);
});
//dispatch_apply函数中的处理全部执行结束
//在Main Dispatch Queue中非同步执行
dispatch_async(dispatch_get_main_queue(),^{
//在Main Dispatch Queue中执行处理
NSLog(@"done");
});
});
10、dispatch_suspend/dispatch_resume
//dispatch_suspend函数挂起指定的Dispatch Queue
dispatch_suspend(queue)
//dispatch_suspend函数恢复指定的Dispatch Queue
dispatch_resume(queue)
- 这些函数对已经执行的处理没有影响,挂起后,追加到Dispatch Queue中但尚未执行的处理,在此之后停止执行,而恢复后使得这些处理能继续执行
11、Dispatch Semaphore
- Dispatch Semaphore是持有计数的信号,该计数是多线程编程中的计数类型信号,所谓信号,类似过马路时常用的手旗,可以通过时举起手旗,不可以通过时放下手旗,在Dispatch Semaphore中,使用计数来实现该功能,计数为0时等待,计数为1或大于1时,减去1而不等待
/*
* 使用dispatch_semaphore_create函数生成Dispatch Semaphore
* 参数表示计数的初始值
* 函数名称中包含create,必须自己通过dispatch_release函数释放,和dispatch_retain函数持有
*/
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
/*
* dispatch_semaphore_wait函数等待Dispatch Semaphore的计数值达到大于或等于1
* 当计数大于等于1,或者待机中计数值大于等于1时,对该计数进行减法并从dispatch_semaphore_wait函数返回
* 第二个参数:等待时间
*/
dispatch_semaphore_wait (semaphore, DISPATCH_TIME_FOREVER);
46EFE57F9F6B581F762BE7E601596DF1.jpg
443268CA18DBEFD31E458C8DBCA3D489.jpg
052A16CBE19578234C65672A9BBA8BC8.jpg
12、dispatch_once
- dispatch_once函数时保证在应用程序执行中只执行一次指定处理的API
总结
《iOS与OS X多线程和内存管理》整本书已经完成了简单的笔记整理,在学习这本书的时候,我以单元为单位,第一遍进行粗略阅读,整体了解该章节的内容,然后第二遍开始仔细阅读并且整理笔记,这样感觉记忆和理解的更为深刻,然后就该章节在配合《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法 》书中对应的内容再次学习,又一次的扩充了该知识点。当然如此经典的书,每一次阅读都会有不同的理解,都会学到不同的东西,有时间我会再学习,有新的理解会及时更新整理文章内容,文笔有限,希望大家包涵,这是我在这次读书时总结的心得经验,希望对大家有帮助