iOS并發編程Tips(一)
關于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
內核數據結構(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 的寫法)。
并發同步
在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。
參考文檔
- https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html
- http://blog.leichunfeng.com/blog/2015/07/29/ios-concurrency-programming-operation-queues/
- http://stackoverflow.com/questions/588866/whats-the-difference-between-the-atomic-and-nonatomic-attributes/589392#589392