关于iOS并发编程, 雷纯锋有篇博客 做了很完整的介绍,大家可以移步学习一下。
我们在这里并不探究 NSThread
、 GCD
、 NSOperation
、 NSOperationQueue
的具体用法,只探讨一些容易被遗忘的小点。
首先,什么是线程,维基百科上是这么说的:
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的内存空间。
Each thread has its own execution stack and is scheduled for runtime separately by the kernel. — Apple Thread Management
按照 苹果官方文档 上的说法,每个线程在内核空间上大概要消耗 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.
在iOS中,主线程的栈空间大小为 1MB , 在OS X中,主线程的栈空间大小为 8MB ,并且,这都是不可修改的。子线程默认栈空间为 512KB 。
栈空间不是立即被创建分配的,它会随着使用而增长。所以说,即使主线程有 1MB 的栈空间,那么,在很大的一段时间之内,你都只会用到很少的一部分。
子线程允许分配的最小栈空间是 16KB ,并且,必须为 4KB 的倍数。我们可以通过 stackSize
属性来修改一个子线程的栈空间:
NSThread *t = [[NSThread alloc] initWithTarget:target selector:selector object:object]; t.stackSize = size;
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
等):
- (void)setCurrentImage:(UIImage *)currentImage { @synchronized(self) { if (_currentImage != currentImage) { _currentImage = currentImage; } } } - (UIImage *)currentImage { @synchronized(self) { return _currentImage; } }
- (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
方法是线程安全的。
我们举个例子,如果现在同时发生:
getter
方法。 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
的写法)。
在GCD上,我们有两种常见方法来让并发程序在某个点上进行同步,分别是 dispatch_group
和 dispatch_barrier
。相比起 dispatch_barrier
,我们可能用到 dispatch_group
的地方会更多一些。
dispatch_group
允许向 group
中添加多个block块,在所有添加的block块全部执行完成之后,再通知其他队列执行其他的方法。而这个完成点就是并发的同步点。
dispatch_group
的写法一般如下:
dispatch_queue_t dispatchQueue = dispatch_queue_create("com.ifujun.text", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t dispatchGroup = dispatch_group_create(); dispatch_group_async(dispatchGroup, dispatchQueue, ^(){ NSLog(@"dispatch-1"); }); dispatch_group_async(dispatchGroup, dispatchQueue, ^(){ NSLog(@"dspatch-2"); }); dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){ NSLog(@"end"); });
dispatch_barrier
就比较有意思了。 dispatch_barrier
是一个障碍点,在并发队列遇到 dispatch_barrier
之后, dispatch_barrier
的block块会被延迟执行,直到所有在它之前提交的block块全部执行完成,然后才会开始执行 dispatch_barrier
的block块。
我们举个例子:
dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 1"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 2"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 3"); }); dispatch_barrier_async(concurrentQueue, ^(){ NSLog(@"barrier"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 4"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 5"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"block 6"); });
上面的代码中,block 1 - 6 都是可以并发执行的,但是由于 barrier
的存在,在block 1 - 3 执行完成之后,才会执行 barrier
,在 barrier
执行完成之后,才会并发执行剩下的block 4 - 6。
执行顺序如下图:
dispatch_barrier
有一个比较常见的用法是读写锁。在上面的 atomic
上,我们说到, atomic
因为给 setter
和 getter
方法加锁,会造成很大的性能浪费,相当于同时只能一个线程在读或者写。
我们要的并不是单读单写,我们要的是多读单写,这样才能确保数据完整并且性能不错。
我们这以缓存举例(缓存必然需要有较高的性能,同时也要支持多读单写),如果用 dispatch_barrier
来实现的话,大概会是这样:
#import "FKCache.h" @interface FKCache () @property (strong, nonatomic) NSMutableDictionary *cacheDictionary; @property (strong, nonatomic) dispatch_queue_t queue; @end @implementation FKCache + (instancetype)shardInstance { static FKCache *cache = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cache = [[FKCache alloc] init]; }); return cache; } - (instancetype)init { if (self = [super init]) { _cacheDictionary = [NSMutableDictionary dictionary]; _queue = dispatch_queue_create("com.ifujun.readwritelock", DISPATCH_QUEUE_CONCURRENT); } return self; } - (void)setObjectForKey:(id)object forKey:(NSString *)key { dispatch_barrier_async(self.queue, ^{ [self.cacheDictionary setObject:object forKey:key]; }); } - (id)objectForKey:(NSString *)key { __block id value = nil; dispatch_async(self.queue, ^{ value = [self.cacheDictionary objectForKey:key]; }); return value; } @end
和 dispatch_group
比起来有一点很大的不同的是, dispatch_group
上添加的block块可以来自于 不同的并发队列 ,而 dispatch_barrier
只会阻塞 同一个并发队列 中的block。