《Objective-C高级编程:iOS与OS X多线程和内存管理》是iOS开发中一本经典书籍,书中有关ARC、Block、GCD的梳理是iOS开发进阶路上必不可少的知识储备。笔者读完此书后为了加强理解,特以笔记记之。本文为完结篇,主要谈论Objective-C中的GCD。
鉴于本书翻译自日文原版且翻译偏向书面,笔者希望采用通俗的语言记录,文章结构略有调整。
GCD概要
多线程编程
想聊GCD得先从多线程说起。
计算机执行应用程序时,先将代码转换成CPU命令列(二进制代码),再将包含在应用程序中的CPU命令列配置到内存中,CPU从应用程序指定的地址开始,一个一个地执行CPU命令列。由于一个CPU核一次只能执行一个CPU命令,不能执行某处分开的并列的两个命令,因此命令列就好比一条无分叉的大道。
- 线程:一个CPU执行的命令列为一条无分叉路径”即为“线程”
- 多线程:随着技术进步,一台计算机可以使用多个CPU核,存在多条这种无分叉路径时即为“多线程”。
使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,看上去好像一个CPU核能够并列执行多个线程一样。而在具有多个CPU核的情况下,真的提供多个CPU核并发执行多个线程的技术,称之为“多线程编程”。
多线程编程容易发生的一些问题:
- 数据竞争:多个线程更新相同的资源会导致数据的不一致。
- 死锁:多个线程互相持续等待。
- 内存消耗过大:使用太多线程会消耗大量内存。
尽管极易发生各种问题,也应当使用多线程编程,因为多线程编程可保证应用程序的响应性能。
主线程与其他线程
- 主线程:启动应用最先执行的线程。用来描绘用户界面,处理触摸屏幕的事件等。
- 其他线程:用来处理耗时操作。例如AR画像识别或数据库访问会阻塞主线程,妨碍主线程RunLoop的执行,从而导致不能更新用户界面、应用画面长时间停滞等问题。应在其他线程执行。
进程与线程
- 进程:系统进行资源分配和调度的基本单位。每个程序的进程相互独立,有着各自的内存空间。
- 线程:程序执行流的最小单元,如上述提到的命令列。
什么是GCD
GCD(Grand Central Dispatch)是异步执行任务的技术之一(iOS中其他多线程技术:pthread、NSThread、NSOperation)。开发者只需要定义想要执行的任务并追加到适当的Dispatch Quue中,GCD就能生成必要的线程并执行。
GCD用基于C语言的API,以非常简洁的记述方法,实现了极为复杂繁琐的多线程编程,通过GCD提供的系统级线程管理可以提高执行效率。
GCD队列
Dispatch Queue:执行处理的等待队列。
串行队列与并发队列
Dispatch Queue的种类:
种类 | 名称 | 说明 | 线程 |
---|---|---|---|
Serial Dispatch Queue | 串行队列 | 等待现在执行中处理结束 | 使用一个线程 |
Concurrent Dispatch Queue | 并发队列 | 不等待现在执行中处理结束 | 使用多个线程 |
- 串行队列同时执行的处理数只有一个,按照顺序执行。
- 并发队列执的行顺序会取决于处理的任务量和系统的状态(CPU核数、CPU负荷等)。
- 多个串行队列可并发执行,每个串行队列都使用各自的一个线程。
并行与并发
并行是并发的子集。通过调度算法实现逻辑上的同步执行属于并发,当多核CPU实现物理上的同步执行才是并行。
队列与线程的关系
队列和线程并非拥有关系,队列是任务容器(一种数据结构),CPU从队列中取出任务,放到对应的线程上去执行。
队列的创建
队列的创建使用dispatch_queue_create
函数。
1 | /** |
1 | // 串行队列 |
当生成多个串行队列时,各个串行队列将并发执行。一旦生成串行队列并追加任务处理,系统对于一个串行队列就只使用一个线程。如果使用过多线程,就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能。
为了避免使用多线程时出现数据竞争的问题,多个线程更新相同资源时可使用串行队列。
并发队列不会出现以上问题,不管生成多少,XNU内核只使用有效管理的线程。
生成的队列必须由程序员负责释放,因为队列并没有像Block那样具有作为Objective-C对象来处理的技术。释放使用
dispatch_release()
方法,与之对应有dispatch_retain()
方法。
在iOS6.0、macOS 10.8及以上系统中,ARC已经能够管理GCD对象了,不应该使用dispatch_release()
和dispatch_retain()
。
主队列与全局队列
除了自行创建外,还可以获取系统标准提供的两种队列。
种类 | 名称 | 所属队列 | 工作线程 |
---|---|---|---|
Main Dispatch Queue | 主队列 | 串行队列 | 主线程 |
Global Dispatch Queue | 全局队列 | 并发队列 | 其他线程 |
- 追加到主队列的任务在主线程的RunLoop中执行,如更新用户界面的处理必须追加到主队列中,与
NSObject
类的performSelectorOnMainThread
实例方法相同。 - 全局队列无需逐个创建并发队列,只要获取使用即可。全局队列有四个优先级,优先级只是大致判断,并不能保证线程的实时性。
优先级 | 创建参数 | 说明 |
---|---|---|
High Priority | DISPATCH_QUEUE_PRIORITY_HIGH | 最高优先级 |
Default Priority | DISPATCH_QUEUE_PRIORITY_DEFAULT | 默认优先级 |
Low Priority | DISPATCH_QUEUE_PRIORITY_LOW | 低优先级 |
Background Priority | DISPATCH_QUEUE_PRIORITY_BACKGROUND | 后台优先级 |
1 | // 主队列 |
对主队列和全局队列执行dispatch_release
和dispatch_retain
函数不会引起任何变化,也不会有任何问题。
dispatch_set_target_queue
变更队列优先级
dispatch_queue_create
函数生成的串行队列和并发队列与默认优先级的全局队列使用相同优先级执行的线程。dispatch_set_target_queue
可以变更生成的队列的优先级。
1 | dispatch_queue_t myQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL); |
防止多个串行队列并发执行
GCD中常使用dispatch_async
函数d非同步(异步,不等待)追加任务到队列中执行。
1 | - (void)dispatch_queue_test_1 { |
输出:
1 | 执行队列:2 |
前面提到多个串行队列可并发执行,以上就是并发执行的结果。 使用dispatch_set_target_queue
函数可以防止多个串行队列并发执行。
1 | - (void)dispatch_queue_test_2 { |
输出:
1 | 执行队列:0 |
GCD函数
dispatch_after
1 | /** |
dispatch_after
函数是在指定时间追加任务到指定队列中,并不是在指定时间后执行任务。想大致延迟任务时,该函数非常有效。
Dispatch Group
Dispatch Group适用于多个任务执行结束后,再执行某个指定的任务。创建任务组使用dispatch_group_create
函数,追加任务使用dispatch_group_async
函数。
dispatch_group_notify
函数。起到监听的作用,当group
中任务完成时可以做一些操作。
1 | - (void)dispatch_notify { |
输出:
1 | 任务1 |
dispatch_group_wait
函数。可以指定gropu
任务超时的时间,无论指定的超时时间和group
中任务完成哪个先到,dispatch_group_wait
函数都会执行并有返回值。返回值为0即指定时间内任务全部完成,不为0则已超时,任务继续。
1 | - (void)dispatch_wait { |
输出:
1 | 任务3 |
将上例中超时时间设置为0.2或更小值,输出:
1 | 已超时,任务仍在继续 |
在dispatch_group_wait
指定超时时间或 group
任务完成之前,执行dispatch_group_wait
函数的当前线程阻塞。推荐使用dispatch_group_notify
函数追加结束任务到队列中,因为 dispatch_group_notify
函数可以简化源代码。
dispatch_barrier_async
避免数据竞争的思路:在写入处理结束之前,读取处理不可执行,写入处理追加到串行队列中,为了提高效率,读取处理追加到并发队列中。
GCD 提供更高效的方法:dispatch_barrier_async函数
。
1 | - (void)dispatch_barrier { |
输出:
1 | 读取操作 |
dispatch_barrier_async
函数如同栅栏一般,使用并发队列和dispatch_barrier_async
函数可实现高效率的数据访问和文件访问。
dispatch_sync
同步与异步
- 同步(sync):任务完成前一直等待。
- 异步(async):不做任何等待。
与dispatch_group_wait
相似,dispatch_sync
的“等待”意味着阻塞当前线程,也可以说是简易版的dispatch_group_wait
。
适用于在主线程中使用其他线程执行任务,任务结束后使用所得到的结果。
1 | - (void)dispatch_sync_0 { |
注意:由于dispatch_sync
会阻塞当前线程,使用不当会引起死锁。
1 | - (void)dispatch_sync_1 { |
1 | - (void)dispatch_sync_2 { |
以上两例都会发生死锁情况。
对死锁的理解
一个或多个线程中出现相互等待的情况就会发生死锁。死锁通常是双向阻塞导致的,具体到使用 GCD 开发中,当
dispatch_sync
和它追加的Block处于同一串行队列时,一定会发生死锁。因为dispatch_sync
会阻塞当前线程,等待Block执行完才会返回,而Block又得等待dispatch_sync
执行完才会执行。
dispatch_apply
dispatch_apply
函数按照指定的次数将Block任务追加到指定的队列中,等待任务完成再执行其他操作。与 dispatch_sync
一样,dispatch_apply
也会阻塞线程。
1 | - (void)dispatch_apply_1 { |
1 | - (void)dispatch_apply_2 { |
dispatch_suspend / dispatch_resume
1 | // 挂起指定队列 |
这些操作不影响已经执行的任务。挂起后,队列中未执行的任务会停止,恢复后这些任务会继续执行。
Dispatch Semaphore
Dispatch Semaphore是持有计数的信号,该信号是多线程编程中的计数类型信号。信号类似于过马路时的手旗,可以通过时举起手旗,不可通过时放下手旗。
Dispatch Semaphore常用以下三个函数:
函数名 | 参数 | 说明 |
---|---|---|
dispatch_semaphore_create |
1个参数:大于等于0的数值 | 创建信号 |
dispatch_semaphore_signal |
1个参数:信号(semaphore) | 信号量 +1 |
dispatch_semaphore_wait |
2个参数:信号(semaphore)和时间 | 信号量 -1,若为0则等待 |
- 信号量用于对资源进行加锁操作。
1 | - (void)dispatch_semaphore_1 { |
- 信号量用于链式请求,限制一个请求完成后再去执行下一个。
1 | - (void)dispatch_semaphore_2 { |
dispatch_once
dispatch_once
函数能保证应用程序中任务只执行一次,该代码在多线程环境下执行可保证百分之百安全。常用于生成单例。
1 | static dispatch_once_t onceToken; |
1 | // 单例 |
以上就是GCD部分的学习内容。