iOS虽然应用有多个线程看起来非常赞,但每个线程都有一定的开销,从而影响到应用的性能。线程不仅仅有创建时的时间开销,还会消耗内核的内存,即应用的内存空间。
内核数据结构
每个线程大约消耗 1KB 的内核内存空间。这块内存用于存储与线程有关的数据结构和属 性。这块内存是联动内存(wired memory),无法被分页。
栈空间
主线程的栈空间大小为 1M,而且无法修改。所有的二级线程默认分配 512KB 的栈空间。 注意,完整的栈并不会立即被创建出来。实际的栈空间大小会随着使用而增长。因此,即使主线程有 1MB 的栈空间,某个时间点的实际栈空间很可能要小很多。
在线程启动前,栈空间的大小可以被改变。栈空间的最小值是 16KB,而且其数值必须是 4KB 的倍数。
- 修改栈空间
+(NSThread *)createThreadWithTarget:(id)target selector:(SEL)selector
object:(id)argument stackSize:(NSUInteger)size {
if( (size % 4096) != 0) { return nil; }
NSThread *t = [[NSThread alloc] initWithTarget:target
selector:selector object:argument];
t.stackSize = size;
return t; }
创建耗时
在 iPhone 6 Plus iOS 8.4 上进行了一项快速测试,展示了线程创建的耗时(不包含启动时间),其区间范围在 4000~5000 微秒,即 4~5 毫秒。创建线程后启动线程的耗时区间为 5~100 毫秒,平均大约在 29 毫秒。这是很大的时间开销,若在应用启动时开启多个线程,则尤为明显。线程的启动时间之所以如此之长,是因为多次的上下文切换所带来的开销。出于简洁的目的,我们省略了计算的代码。要想了解细节,你可以参考GitHub(https://github.com/gvaish/hpios/blob/master/src/ViewControllers/HPChapter05ViewController.m)中的 computeThreadCreationTime 方法。
GCD
GCDAPI(https://developer.apple.com/library/mac/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html)由核心语言特性、运行时库以及对执行并行代码的系统增强所组成
GCD 提供的功能列表:
• 任务或分发队列,允许主线程中的执行、并行执行和串行执行。
• 分发组,实现对一组任务执行情况的跟踪,而与这些任务所基于的队列无关。
• 信号量。
• 屏障,允许在并行分发队列中创建同步的点。
• 分发对象和管理源,实现更为底层的管理和监控。
• 异步 I/O,使用文件描述符或管道。
GCD 同样解决了线程的创建与管理。它帮助我们跟踪应用中线程的总数,且不会造成任何 的泄漏。
大多数情况下,应用单独使用 GCD 就可以很好地工作,但仍有特定的情况 需要考虑使用 NSThread 或 NSOperationQueue。当应用中有多个长耗时的任 务需要并行执行时,最好 . 对线程的创建过程加以控制。如果代码执行的时 间过长,很有可能达到线程的限制 64 个,2,3 即 GCD 的线程池上限。 应该避免浪费地使用 dispatch_async 和 dispatch_sync,因为那会导致应用 崩溃 4。虽然 64 个线程对移动应用来说是个很高的合理值,但不加控制的应 用迟早会超出这个限制。
操作与队列
操作和操作队列是 iOS 编程中和任务管理有关的又一个重要概念。
NSOperation 封装了一个任务以及和任务相关的数据和代码,而 NSOperationQueue 以先入先出的顺序控制了一个或多个这类任务的执行。
NSOperation 和 NSOperationQueue 都提供控制线程个数的能力。可用 maxConcurrentOpera- tionCount 属性控制队列的个数,也可以控制每个队列的线程个数。
在使用 NSThread(开发人员管理全部并发)和 GCD(OS 管理并发)之间存在两个选择。以下是对 NSThread、NSOperationQueue 和 GCD API 的一个快速比较。
GCD
♦ 抽象程度最高。
♦ 两种队列开箱即用:main 和 global。
♦ 可以创建更多的队列(使用 dispatch_queue_create)。
♦ 可以请求独占访问(使用 dispatch_barrier_sync 和 dispatch_barrier_async)。
♦ 基于线程管理。
♦ 硬性限制创建 64 个线程。
NSOperationQueue
♦ 无默认队列。
♦ 应用管理自己创建的队列。
♦ 队列是优先级队列。
♦ 操作可以有不同的优先级(使用 queuePriority 属性)。
♦ 使用 cancel 消息可以取消操作。注意,cancel 仅仅是个标记。如果操作已经开始 执行,则可能会继续执行下去。
♦ 可以等待某个操作执行完毕(使用 waitUntilFinished 消息)。
NSThread
♦ 低级别构造,最大化控制。
♦ 应用创建并管理线程。
♦ 应用创建并管理线程池。
♦ 应用启动线程。
♦ 线程可以拥有优先级,操作系统会根据优先级调度它们的执行。
♦ 无直接 API 用于等待线程完成。需要使用互斥量(如 NSLock)和自定义代码。
线程安全的代码
贯穿软件开发的职业生涯,我们总是被教导要编写线程安全的代码,这也就是说,如果有多个线程并行地执行同一组指令,不能产生任何副作用。以下两大类技术可以实现这一点。
• 不要使用可修改的共享状态。
• 如果无法避免使用可修改的共享状态,则确保你的代码是线程安全的。
这些技术说起来容易做起来难。要实现它们有多种选择。 因为应用会包含可修改的共享状态,所以我们需要掌握管理和修改共享状态的最佳实践。 驱动这些最佳实践的一条基本规则是“在代码中保留不变量”。
使用 @synchronized 指令可以创建一个信号量,并进入临界区,临界区在任何时刻都只能 被一个线程执行
@implementation HPUpdaterService
-(void)updateUser:(HPUser *)user properties:(NSDictionary *)properties {
@synchronized(user) {
NSString *fn = [properties objectForKey:@"firstName"]; if(fn != nil) {
user.firstName = fn;
}
NSString *ln = [properties objectForKey:@"lastName"]; if(ln != nil) {
user.lastName = ln;
}
} }
@end
锁
锁是进入临界区的基础构件。atomic 属性和 @synchronized 块是为了实现便捷实用的高级别抽象。
以下是三种可用的锁。
• NSLock
这是一种低级别的锁。一旦获取了锁,执行则进入临界区,且不会允许超过一个线程并 行执行。释放锁则标记着临界区的结束。
@interface ThreadSafeClass () {
NSLock *lock;
}
@end
-(instancetype)init {
if(self = [super init]) {
self->lock = [NSLock new]; }
return self;
}
-(void)safeMethod {
[self->lock lock];
//线程安全的代码
[self->lock unlock];
}
• NSRecursiveLock
在调用 lock 之前,NSLock 必须先调用 unlock。但正如名字所暗示的那样, NSRecursiveLock 允许在被解锁前锁定多次。如果解锁的次数与锁定的次数相匹配,则 认为锁被释放,其他线程可以获取锁。
当类中有多个方法使用同一个锁进行同步,且其中一个方法调用另一个方法时, NSRecursiveLock 非常有用。
@interface ThreadSafeClass () {
NSRecursiveLock *lock;
}
@end
-(instancetype)init {
if(self = [super init]) {
self->lock = [NSRecursiveLock new];
}
return self;
}
-(void)safeMethod1 {
[self->lock lock];
[self safeMethod2];
[self->lock unlock];
}
-(void)safeMethod2 {
[self->lock lock];
//线程安全的代码
[self->lock unlock]; }
• NSCondition
有些情况需要协调线程之间的执行。例如,一个线程可能需要等待其他线程返回结果。 NSCondition 可以原子性地释放锁,从而使得其他等待的线程可以获取锁,而初始的线 程继续等待。 一个线程会等待释放锁的条件变量。另一个线程会通知条件变量释放该锁,并唤醒等待中的线程。
有兴趣学习的可以加群一起来探讨: 里面也有一些开发相关的PDF文档