在iOS并发编程中经常会遇到一些问题,我们在这里并不探究 NSThread 、 GCD 、 NSOperation 、 NSOperationQueue 的具体用法,只探讨一些容易被遗忘的小点。希望对广大iOS开发者能够起到一定的帮助。
线程成本
维基百科上对线程的解释是:
A thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler.
通俗来说,线程(thread),指的是一个独立的代码执行路径,也就是说线程是代码执行路径的最小分支。在 iOS 中,线程的底层实现是基于 POSIX threads API 的,也就是我们常说的 pthreads。
在iOS中,进程启动之后,一个最主要的线程我们称为主线程。主线程会创建和管理所有的UI元素。一般来说,与用户交互相关的中断性操作都会派发到主线程上进行处理,包括你的 IBAction 的方法。
线程的创建是需要成本的,每个线程不仅仅在创建的过程中需要耗费时间,同时,它也会占用一定的内核的内存空间和app的内存空间。
有关iOS多线程开发更详细的内容推荐观看:iOS多线程及异步任务处理(http://www.maiziedu.com/course/ios/23-2525/)
内核数据结构(Kernel data structures)
据官方文档,每个线程在内核空间上大概要消耗 1KB 大小的内存。而这块内存是用于存储线程的数据结构和属性的。这是一个连系内存(wired memory),不能在磁盘上分页。
This memory is used to store the thread data structures and attributes, much of which is allocated as wired memory and therefore cannot be paged to disk.
线程栈空间大小(Stack space)
在iOS中,主线程的栈空间大小为 1MB , 在OS X中,主线程的栈空间大小为 8MB ,并且,这都是不可修改的。子线程默认栈空间为 512KB 。
栈空间不是立即被创建分配的,它会随着使用而增长。所以说,即使主线程有 1MB 的栈空间,那么,在很大的一段时间之内,你都只会用到很少的一部分。
子线程允许分配的最小栈空间是 16KB ,并且,必须为 4KB 的倍数。我们可以通过 stackSize 属性来修改一个子线程的栈空间:
NSThread *t = [[NSThread alloc] initWithTarget:target
selector:selector object:object];
t.stackSize = size;
线程创建时间(Creation time)
The figures were determined by analyzing the mean and median values generated during thread creation on an Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running OS X v10.5.
据官方文档,在一个2GHz的双核Intel处理器、1GB内存、OS X 10.5系统的iMac上,需要花费 90微秒 的时间(有些人会写90ms或者是90毫秒,其实,这里的ms是microsecond,而不是millisecond)。
原子属性
在声明属性的时候,我们两种选择,一种是 atomic ,一种是 nonatomic ,前者是原子的,后者是非原子的。基本上,他们的区别就在于, atomic 会在属性的 setter 方法上加上一个互斥锁(也 有一种说法 是使用自旋锁spin locks,不过,由于 自旋锁的bug ,可能苹果并不会使用自旋锁,转而使用 pthread_mutex 或者 dispatch_semaphore 等):
atomic
- (void)setCurrentImage:(UIImage *)currentImage
{
@synchronized(self) {
if (_currentImage != currentImage) {
_currentImage = currentImage;
}
}
}
- (UIImage *)currentImage
{
@synchronized(self) {
return _currentImage;
}
}
nonatomic
- (void)setCurrentImage:(UIImage *)currentImage
{
if (_currentImage != currentImage) {
_currentImage = currentImage;
}
}
- (UIImage *)currentImage
{
return _currentImage;
}
属性默认是 atomic 修饰的,明确写 nonatomic 才会是非原子操作。
比如:
@property(nonatomic, strong) UITextField *userName;
@property(atomic, strong) UITextField *userName;
@property(strong) UITextField *userName;
后两者其实是一样的,只有第一种才是非原子操作。
是否线程安全?
从上面的代码来看, atomic 最多也就只能保证属性的 setter 和 getter 方法是线程安全的。
我们举个例子,如果现在同时发生:
线程A在调用 getter 方法。
线程B、线程C在调用 setter 方法,并且它们设置的值是不一致的。
那么,线程A可能会获得原来的值,也可能会获得线程B或者线程C的值,这是不一定的。而且,属性最终的值可能是线程B,也可能是线程C设置的值。
用《 Effective Objective-C 2.0 》上面的话说,就是:
这么做虽然能提供某种程度的“线程安全”,但却无法保证访问该对象时绝对是线程安全的。当然,访问属性的操作确实是“原子”的。使用属性时,必定能从中获取到有效值,然而在同一个线程上多次调用获取方法,每次获取到的结果却未必相同。在两次访问操作之间,其他线程可能会写入新的值。
所以,要说到真正的线程安全, atomic 的差距还是有点大的。
是否应该使用?
在没有资源竞争的情况下(比如,单线程的时候), atomic 可能还是很快的,但是 在比较普遍的情况下, atomic 想比起 nonatomic 可能会有靠近20倍的性能差异, stack overflow中有人对此进行了测试 。
那么,究竟是否该使用 atomic 呢,这个要看你是否需要。对我来说,我一般很少使用 atomic ,如果实在有需要的话,我一般会使用 dispatch_barrier 代替(具体例子可以参考下面的 dispatch_barrier 的 setter 和 getter 的写法)。
有关并发同步的问题,下期文章将会举实际的例子来讲。