多线程是一种十分常用的技术,但它也容易发生各种问题。比如多个线程更新相同的资源会导致数据的不一致(数据竞争)、停止等待事件的线程会导致多个线程相互持续等待(死锁)、使用太多线程会消耗大量内存等。
iOS 目前有4套多线程方案:
- pthread
- NSThread
- GCD
- NSOperation
pthread
pthread 是一套 C 语言的接口,虽然因此具备很强的移植性,但是它用起来不怎么方便:
|
|
NSThread
看名字就明白,NSThread 是用 Objective-C 封装过的一套接口,实现了面向对象。用来要比 pthread 方便一点,但它的生命周期还是需要我们手动管理。
先创建线程类,再启动
|
|
创建并自动启动
|
|
使用 NSObject 的方法创建并自动启动
|
|
其他方法
|
|
GCD
Grand Central Dispatch(GCD)是异步执行任务的技术之一。开发者只需要定义想执行的任务并追加到适当的 Dispatch Queue 中,GCD 就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前的线程更有效率。
在 GCD 之前,Cocoa 框架提供了 NSObject 类的performSelectorInBackground:withObject
实例方法和performSelectorOnMainThread
实例方法等简单的多线程编程技术。这类方法要比使用 NSThread 类进行多线程编程简单,但 GCD 更为简洁,且 GCD 提供的系统级线程管理提高执行效率。
Dispatch Queue
开发者要做的只是定义想执行的任务并追加到适当的 Dispatch Queue 中。
“Dispatch Queue”就是执行处理的等待队列,它按照追加的顺序(FIFO)执行处理。 有两种 Dispatch Queue:
- 等待现在执行中处理的 Serial Dispatch Queue
- 不等待现在执行中处理的 Concurrent Dispatch Queue
简单的说,一个是串行队列而另一个是并行队列。并行执行的处理数量取决于当前系统的状态(iOS 和 OS X 基于 Dispatch Queue 中的处理数、CPU 核数以及 CPU 负荷等当前系统的状态来决定 Concurrent Dispatch Queue 中并行执行的处理数)
获取 Dispatch Queue
第一种方法是通过 GCD 的 API 生成 Dispatch Queue。
|
|
第一个参数指定 Serial Dispatch Queue 的名称,用于调试。第二个参数指定为 NULL 生成 Serial Dispatch Queue,生成 Concurrent Dispatch Queue 时,指定为DISPATCH_QUEUE_CONCURRENT
。
dispatch_queue_create
函数可生成任意多个 Dispatch Queue。
由于 ARC 只对 Objective-C 对象有效,所以通过 dispatch_queue_create 函数生成的 Dispatch Queue 在使用结束后要通过dispatch_release
函数释放。
|
|
相应也存在dispatch_retain
函数。
|
|
系统对于一个 Serial Dispatch Queue 就只生成并使用一个线程,但不可大量生成,如果过多使用多线程,就会消耗大量内存,大幅度降低系统性能。只在为了避免多线程编程问题之一——多个线程更新相同资源导致数据竞争时使用 Serial Dispatch Queue。当想并行执行不发生数据竞争等问题的处理时,使用 Concurrent Dispatch Queue。对于后者来说,不管生成多少,由于 XNU 内存只使用有效管理的线程,因此不会发生 Serial Dispatch Queue 的那些问题。
第二种方法是获取系统标准提供的 Dispatch Queue:Main Dispatch Queue
和 Global Dispatch Queue
。
Main Dispatch Queue 是在主线程中执行的 Dispatch Queue,它是 Serial Dispatch Queue。
Global Dispatch Queue 是所有应用程序都能够使用的 Concurrent Dispatch Queue,它有4个执行优先级,分别是高优先级(High Priority)、默认优先级(Default Priority)、低优先级(Low Priority)和后台优先级(Background Priority)。优先级只能做大致的判断。
|
|
对于 Main Dispatch Queue 和 Global Dispatch Queue 执行 dispatch_retain 函数和 dispatch_release 函数不会引起任何变化,也不会有任何问题。
dispatch_set_target_queue
dispatch_queue_create 函数生成的 Dispatch Queue 不管是 Serial Dispatch Queue 还是 Concurrent Dispatch Queue,都使用与默认优先级 Global Dispatch Queue 相同执行优先级的线程。可通过dispatch_set_target_queue
变更优先级。
|
|
第一个参数为要变更执行优先级的 Dispatch Queue,第二个参数是与要使用的执行优先级相同优先级的 Global Dispatch Queue。
dispatch_set_target_queue 还可以改变 Dispatch Queue 的执行层次。如果在多个 Serial Dispatch Queue 中用 dispatch_set_target_queue 函数指定目标为某个 Serial Dispatch Queue,那么原本应并行执行的多个 Serial Dispatch Queue,在目标 Serial Dispatch Queue 上只能同时执行一个处理。一般都是把一个任务放到一个串行的queue中,如果这个任务被拆分了,被放置到多个串行的queue中,但实际还是需要这个任务同步执行,那么就会有问题,因为多个串行queue之间是并行的。这时候dispatch_set_target_queue将起到作用。
dispatch_after
在3秒后执行处理:
|
|
dispatch_after
函数并不是在指定时间后执行处理,而是在指定时间追加处理到 Dispatch Queue。
第一个参数是dispatch_time_t
类型的值,该值使用dispatch_time
函数或dispatch_walltime
函数作成,这两者分别用于计算相对时间与绝对时间。dispatch_time
函数能够获取从第一个参数指定的时间开始,到第二个参数指定的毫微秒单位时间后的时间。如果使用NSEC_PER_MSEC
则可以以毫秒为单位计算。
Dispatch Group
在追加到 Dispatch Queue 中的多个处理全部结束后想执行结束处理,这种情况可以使用 Dispatch Group。
|
|
另外,在 Dispatch Group 中也可以使用 dispatch_group_wait 函数仅等待全部处理执行结束。
|
|
dispatch_group_wait
函数的第二个参数指定为等待的时间(超时)。DISPATCH_TIME_FOREVER
意味着永久等待。也可以指定等待1秒:
|
|
”等待“意味着 dispatch_group_wait 函数处理调用状态而不返回,即在当前线程停止。
dispatch_barrier_async
为了避免读写数据时的竞争问题,并使读能够并行处理,可以使用dispatch_barrier_async
函数。
|
|
这样,dispatch_barrier_async
函数会等待追加到 Concurrent Dispatch Queue 上的并行执行的处理全部结束之后,然后由 dispatch_barrier_async 函数追加的处理执行完毕后,Concurrent Dispatch Queue 才恢复一般的动作。
dispatch_sync
dispatch_sync
是将指定的 Block”同步“追加到指定的 Dispatch Queue 中。在追加 Block 结束之前,dispatch_sync
函数会一直等待。
不过这个方法稍有不慎就会导致死锁:
|
|
dispatch_apply
dispatch_apply
函数式 dispatch_sync 函数和 Dispatch Group 的关联 API。该函数按指定的次数将指定的 Block 追加到指定的 Dispatch Queue 中,并等待全部处理执行结束。
|
|
执行结果为
|
|
因为在 Global Dispatch Queue 中执行处理,所以各个处理的执行时间不定。但是 done 必定在最后的位置上,因为 dispatch_apply 函数会等待全部处理执行结束。
dispatch_suspend / dispatch_resume
dispatch_suspend 函数挂起指定的 Dispatch Queue
dispatch_resume 函数恢复指定的 Dispatch Queue
这些函数对已经执行的处理没有影响。挂起后,追加到 Dispatch Queue 中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。
Dispatch Semaphore
Dispatch Semaphore 是持有计数的信号,计数为0时等待,计数为1或大于1时,减去1而不等待。
dispatch_once
dispatch_one 函数是保证在应用程序执行中只执行一次指定处理的 API,在多线程环境下也可保证百分之百安全。一般用于单例模式。
Dispatch I/O
使用 Dispatch I/O 和 Dispatch Data 可以在读取大文件时,将文件分成合适的大小进行并行读取,提高读取速度。
NSOperation
其实 NSOperation 比 GCD 早诞生,但是后来 NSOperation 又重新基于 GCD 进行了封装。NSOperationQueue 对应于 GCD 的队列。
NSOperation 是一个抽象类,不能实例化,所以需要使用它的两个子类:NSInvocationOperation
和NSBlockOperation
。创建一个 operation 后,需要调用 start 方法来启动任务,它会默认在当前队列同步执行。
|
|
其中,NSBlockOperation
还有一个方法addExecutionBlock:
,通过这个方法可以个 Operation 添加多个执行 Block,被添加的任务会在主线程和其他线程中并发执行。
运行队列
NSOperationQueue 分为主队列和其他队列。主队列可以直接获取,而自己创建的队列则属于其他队列,其他队列上的任务会在其他线程上执行。
|
|
被添加到 NSOperationQueue 的任务会自动并行执行,如果想要达到串行的目的,可以将 NSOperationQueue 的参数maxConcurrentOperationCount
设置为1。
相比 GCD
NSOperation 还有添加依赖,比如有 3 个任务:A: 从服务器上下载一张图片,B:给这张图片加个水印,C:把图片返回给服务器。
|
|
此外,我们能将 KVO 应用在 NSOperation 中,可以监听一个 Operation 是否完成或取消,这样子能比 GCD 更加有效地掌控我们执行的后台任务。也可以针对 NSOperation 设置优先级。
NSOperation 作为对象,能够让开发者应用更多面向对象的方式进行管理和开发,如通过继承给 NSOperation 添加属性和方法,达到更好复用的目的。
线程同步
互斥锁
|
|
GCD 中的方法
如 dispatch_barrier_async
、Dispatch Semaphore
。
同步执行
将任务添加到串行队列中进行执行。
参考资料
- 《Objective-C 高级编程》
- 关于iOS多线程,你看我就够了
- NSOprationQueue 与 GCD 的区别与选用