多线程之GCD
2015-04-19 本文已影响7523人
冷漠叻荭颜
Runloop
-
什么是Runloop
- Runloop就是消息循环,每一个线程内部都有一个消息循坏。
- 只有主线程的消息循环默认开启,子线程的消息循环默认不开启。
-
消息循环的目的
- 保证程序不退出。
- 负责处理输入事件。Runloop接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source)。
- 如果没有事件发生,会让程序进入休眠状态。
-
使用消息循环的时候必须指定两件事情
- 输入事件:输入源和定时源。
- 消息循环模式,此模式必须跟当前消息循环使用的模式匹配。
-
消息循环模式
- NSDefaultRunLoopMode。
- NDRunLoopCommonModes。
-
子线程的Runloop
- 启动子线程的消息循环
GCD
-
什么是GCD
- 全称是Grand Center Dispatch(CPU调度中心,调度的是任务和线程,把任务交给线程执行)。
- 纯C语言,提供了非常多、强大的函数。
- GCD是 libdispatch的统称,而libdispatch是苹果为了在多核硬件(如iOS、OSX)上提供处理并发代码的库。
-
GCD的优势
- GCD是苹果公司为多核的并行运算提出的解决方案。
- GCD会自动利用更多的CPU内核。
- GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。
- 只要告诉GCD想要执行什么任务,不需要编写任何线程管理的代码。
- GCD能帮助你分配繁重计算任务并且保持在后台运行,帮你提高App的响应程度。
- GCD帮你提供易用的并发模式,不仅仅是锁和线程,帮助你避免在开发并发程序时的bug。
- GCD具有在常见模式(如单例模式)以最原始的语句优化,是你的代码具有更高的效率。
-
GCD术语
- 串行、并行:所谓串行并行描述是相对而言的,串行是指在同一时间只执行一个任务,并行是指在同一时间可能执行多个任务。
- 同步、异步:在GCD中,同步异步是为了描述一个函数相对于该函数要求GCD执行完成的另一个任务。同步方法只在它完成它需要做的任务后才会返回。异步方法刚好和同步方法相反,它不会等待任务完成才返回,它会立即返回。所以异步不会阻塞当前线程执行另一个任务(方法\函数)。
- 死锁:两个(有时更多)东西——在大多数情况下,是线程——所谓的死锁是指它们都卡住了,并等待对方完成或执行其它操作。第一个不能完成是因为它在等待第二个的完成。但第二个也不能完成,因为它在等待第一个的完成。
- 线程安全:线程安全的代码在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏、崩溃等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是NSDictionary,你可以在同一时间在多个线程中使用它而不会有问题,另一方面,NSMutableDictionary就不是线程安全的,应该保证一次只能一个线程访问它。
-
GCD使用步骤
- 定制任务:确定要执行的操作(方法)。
- 将任务添加到队列中,并按照指定的同步或异步方式执行任务。
- GCD会自动将队列中的任务取出,放到对应的线程中执行。
- 任务的取出遵循队列的先进先出原则。
-
队列
- GCD提供了dispatch queues(调度队列)来执行代码段,这些队列以FIFO(先进先出)的方式来管理你用GCD提交的任务。这保证了你先提交的任务先执行,即第一个任务添加到队列中就第一个开始执行,第二个添加的任务将第二个执行,直到队列的最后一个任务。
- 所有的调度队列(dispatch queues)自身都是线程安全的,你能通过多个现成来访问它们。
- 串行队列:在串行队列中,同一个队列中只有一个任务在执行,每个任务只有在前一个任务执行完成后才能开始执行。而且,你不知道在一个Block(任务)执行结束到下一个Block(任务)开始执行之间的这段时间是多长。这些任务的执行时机完全是在GCD的控制之下,你能唯一保证的是:GCD一次只能执行一个任务以及它会按照你添加到队列中的先后顺序来执行任务。在串行队列中,不会有两个任务并发执行。
- 并发队列:在并发队列中,你唯一能保证的是,这些任务会按照被添加的顺序开始执行,但是任务可以以任何顺序完成,你不知道在执行下一个任务是从什么时候开始,或者说任意时刻有多个Block(任务)执行,这个完全是取决于GCD。
- 在什么时候执行Block完全是由GCD来决定,如果一个Block的执行时间与另外一个Block重叠,GCD就会决定是否将另一个任务运行在不同的核上,除非那个核不能使用,否则GCD将通过切换上下文方式将任务运行在另一个核上。
-
GCD的核心概念
- GCD的核心概念:
- 任务:要执行的操作(方法)。
- 队列:用来存放任务的集合。
- 将任务添加到队列,指定任务执行的方法。
- 任务:使用block封装,block就是一个提前准备好的代码块,在需要的时候执行。
-
队列:
- 串行队列:一个接一个的调度任务。
- 并发队列:可以同时调度多个任务。
- 主队列:全局串行队列,由主线程串行调度任务,并且只有一个。
- 全局队列:没有名称的并发队列。
- 执行任务的函数:
- 同步执行:当前指令不完成,就不会执行下一条指令。
- 异步执行:当前指令不完成,同样可以执行下一条指令。
- GCD的核心概念:
队列和执行的几种情况
- 串行队列,同步任务
- 不新开线程,顺序(同步)执行。
- 在当前线程执行。
- 串行队列,异步任务
- 开一个线程,顺序(同步)执行。
- 只有一个线程,因为是串行队列,只有一个线程就可以按顺序执行队列中的所有任务。
- 并发队列,异步任务
- 开多个线程,异步执行。
- 每次开启多少个线程是不固定的(线程数,不由我们控制)。
- 线程数由GCD来决定的。
- 并发队列,同步任务
- 不新开线程,顺序(同步)执行。
- 和串行队列同步执行效果相同。
- 主队列,异步任务
- 不新开线程,同步执行。
- 主队列特点:如果主线程正在执行代码暂时不调度任务,等主线程执行结束后再执行任务。
- 主队列又叫全局串行队列,主队列的任务都由主线程来调度,不会开启新线程。
- 主队列,同步任务
- 程序执行不出来,造成“死锁”。
- 死锁的原因,当程序执行到这段代码的时候:
- 主队列:如果主线程正在执行代码,就不调度任务。
- 同步任务:如果第一个任务没有执行,就继续等待第一个任务执行完成,再执行下一个任务。
- 此时互相等待,程序无法往下执行(死锁)。
- 主队列和串行队列的区别:
- 串行队列:必须等待一个任务执行完成,再调度另一个任务。
- 主队列:以先进先出调度任务,如果主线程上有代码在执行,主队列不会调度任务。
- 解决死锁
- 将(主队列,同步任务)放入非主队列异步任务中,可以解决死锁。
- (主队列,同步任务)在子线程中运行,同步执行不用等待主线程执行此同步执行的任务。
- 总结
同步\异步 | 全局并行队列 | 手动创建串行队列 | 主队列 |
---|---|---|---|
同步(sync) | 没有开启新线程 串行执行任务 | 没有开启新线程 串行执行任务 | 会死锁 |
异步(async) | 有开启新线程 并行执行任务 | 有开启新线程 并行执行任务 | 没有开启新线程 串行执行任务 |
同步任务
-
同步任务的作用
- 在网络开发中,通常会把很多任务放在后台异步执行,有些任务会彼此“依赖”,应该使用同步任务处理。
-
同步任务的特点
- 队列调度多个异步任务前,指定一个或者多个同步任务,让所有的异步任务都等待同步任务完成,这就是所谓的“依赖”关系。
全局队列
- 全局队列
- 全局队列的本质就是并发队列。
- 获取全局队列最简单方式
dispatch_get_global_queue(0,0);
。
// 获取全局队列的函数
dispatch_get_global_queue(long identifier, unsigned long flags);
// 第一个参数 identifier
// 在iOS7中表示调度的优先级(让线程响应的更快还是更慢)
DISPATCH_QUEUE_PRIORITY_HIGH 2 高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT 0 默认优先级
DISPATCH_QUEUE_PRIORITY_LOW (-2) 低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 后台优先级
// 在iOS8中表示服务质量
QOS_CLASS_USER_INTERACTIVE 用户希望线程快点执行完毕 不要使用耗时操作
QOS_CLASS_USER_INITIATED 用户需要的 不要使用耗时操作
QOS_CLASS_DEFAULT 默认
QOS_CLASS_UTILITY 耗时操作
QOS_CLASS_BACKGROUND 后台
QOS_CLASS_UNSPECIFIED 0 未指定优先级
// 为了在iOS7和iOS8中适配此参数 可以直接传入0
DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED
DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY
DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND
// 第二个参数 为将来保留使用 始终传入0
- 全局队列和并发队列的区别
- 并发队列有名称,可以跟踪错误。全局队列没有名称。
- 在ARC中不需要考虑释放内存,
dispatch_release(q);
不允许调用。 - 在MRC中需要手动管理内存,并发队列是create创建出来的,在MRC中见到create就要release,全局队列则不需要release,全局队列只有一个。
- 一般我们使用全局队列。
延时操作
- 延时操作
// dispatch_after函数的定义
dispatch_after(dispatch_time_t when,
dispatch_queue_t queue,
dispatch_block_t block);
// dispatch_after的参数
// 参数1 dispatch_time_t when 多少纳秒之后执行
// 参数2 dispatch_queue_t queue 任务添加到哪个队列
// 参数3 dispatch_block_t block 要执行的任务
指定串行队列延时操作
一次性执行
- 程序中有的时候希望一个方法只被执行一次
- 让dispatch_once执行在子线程上
- dispatch_once是线程安全的
- 多个线程的时候也是执行一次。
调度组Dispatch Group
-
Dispatch Group调度组
- Dispatch Group会在被添加进调度组的所有任务都完成时通知你,这些任务可以是同步的,也可以是异步的,即便在不同的队列也行。
- Dispatch Group调度组只有异步执行。
- 创建一个新的Dispatch Group,它的作用就像一个用于未完成任务的计数器。
-
dispatch_group_enter
手动通知Dispatch Group任务已经开始,必须保证dispatch_group_enter
和dispatch_group_leave
成对出现,相当于确保进入Group的次数和离开Group的次数相等,否则可能会遇到诡异的崩溃问题。 - 包含不同队列类型的调度组Dispatch Group
- 自定义串行队列:它很适合当一组任务完成时发出通知。
- 主队列(串行):它也很适合这种情况。但如果你要同步地等待所有工作地完成,就不应该使用,因为不能阻塞主线程,会导致死锁。然而,主队列的异步任务能用于在几个较长任务完成后更新UI的方式。
- 并发队列:它也很适合Dispatch Group和完成时通知。
-
dispatch_group_wait
- 会阻塞当前线程,直到组里面所有的任务都完成或者等到某个超时完成。
- 如果在所有任务完成前超时了,该函数会返回一个非零值。可以对此返回值做条件判断以确定是否超出等待周期。
DISPATCH_TIME_FOREVER
让这个组永远等待。
-
dispatch_apply
- dispatch_apply就像一个for循环,但它能并发地执行不同的迭代。
- 这个函数是同步执行的,所以和普通的for循环一样,它只会在所有工作完成后才返回。
- 对于并发循环使用dispatch_apply,可以帮助你追踪任务的进度。
-
dispatch_group_notify
- 以异步的方式工作。
- 不会阻塞任何线程。
- 有时候需要在多个异步任务都执行完成之后继续做某些事情。