本文整理了Objective-C开发多线程应用的常用方法。
performSelectors系列
NSObject 提供了以 performSelector 为前缀的一系列方法。它们可以让用户在指定线程中,或者立即,或者延迟执行某个方法调用。
在当前线程中执行方法
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument
afterDelay:(NSTimeInterval)delay
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:
(NSTimeInterval)delay inModes:(NSArray *)modes
在指定线程中执行方法
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread
withObject:(id)arg waitUntilDone:(BOOL)wait
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:
(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array
在主线程中执行方法
- (void)performSelectorOnMainThread: (SEL)selector withObject:(id)argument
waitUntilDone:(BOOL)wait
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg
waitUntilDone:(BOOL)wait modes:(NSArray *)array
在后台线程中执行方法:
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
NSThread
NSThread 是 OS X 和 iOS 都提供的一个线程对象,它是线程的一个轻量级实现。在执行一些轻量级的简单任务时,NSThread 很有用,但用户仍然需要自己管理线程生命周期,进行线程间同步。比如,涉及到线程间同步仍然需要配合使用 NSLock,NSCondition 或者 @synchronized。所以,遇到复杂任务时,轻量级的 NSThread 可能并不合适。
动态线程对象
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(loadImageSource:) object:imgUrl];
thread.threadPriority = 1;// 设置线程的优先级(0.0 - 1.0,1.0最高级)
[thread start];
静态方法
[NSThread detachNewThreadSelector:@selector(loadImageSource:) toTarget:self withObject:imgUrl];
常用方法
- 获取当前线程
NSThread *current = [NSThread currentThread];
- 获取主线程
NSThread *main = [NSThread mainThread];
- 暂停当前线程
[NSThread sleepForTimeInterval:2];
NSOperation
NSOperation 做的事情比 NSThread 更多一些。通过继承 NSOperation,可以使子类获得一些线程相关的特性,进而可以安全地管理线程生命周期。比如,以线程安全的方式建立状态,取消线程。配合 NSOperationQueue,可以控制线程间的优先级和依赖性。这就给出了一套线程管理的基本方法。
NSOperation 代表了一个独立的计算单元。一般,我们会把计算任务封装进 NSOperation 这个对象。NSOperation 是抽象类,但同时也提供了两个可以直接使用的实体子类:NSInvocationOperation 和 NSBlockOperation。NSInvocationOperation 用于将计算任务封装进方法,NSBlockOperation 用于将计算任务封装进 block。
NSOperationQueue 则用于执行计算任务,管理计算任务的优先级,处理计算任务之间的依赖性。NSOperation 被添加到 NSOperationQueue 中之后,队列会按优先级和进入顺序调度任务,NSOperation 对象会被自动执行。
- (void)run {
while (TRUE) {
[_lock lock];
if(_cake > 0){
[NSThread sleepForTimeInterval:0.5];
_cake--;
_occupied = kSum - _cake;
NSLog(@"Taken by %@\nCurrent free:%ld, occupied: %ld", [[NSThread currentThread] name], _cake, _occupied);
}
[_lock unlock];
}
}
// test
_lock = [[NSLock alloc] init];
NSInvocationOperation *geroge = [[NSInvocationOperation alloc]initWithTarget:self
selector:@selector(run:) object:@"Geroge"];
geroge.queuePriority = NSOperationQueuePriorityHigh;
NSInvocationOperation *operationTwo = [[NSInvocationOperation alloc]initWithTarget:self
selector:@selector(run:) object:@"Totty"];
totty.queuePriority = NSOperationQueuePriorityLow;
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:2];
[queue addOperation:geroge];
[queue addOperation:totty];
GCD (Grand Central Dispatch)
GCD 是 Apple 公司为了提高 OS X 和 iOS 系统在多核处理器上运行并行代码的能力而开发的一系列相关技术,它提供了对线程的高级抽象。GCD 是一整套技术,包含了语言级别的新功能,运行时库,系统级别的优化,这些一起为并发代码的执行提供了系统级别的广泛优化。所以,GCD 也是 Apple 推荐的多线程编程工具。
GCD 是系统层面的技术,除了可以被系统级应用使用,也可以被更普通的高级应用使用。使用 GCD 之后,应用就可以轻松地在多核系统上高效运行并发代码,而不用考虑繁琐的底层问题。GCD 在系统层面工作,能很好地满足所有应用的并行运行需求,将可用系统资源平衡地分配给它们。
GCD 提供了一套纯 C API。但是,它提供的 API 简单易用并且有功能强大的任务管理和多线程编程能力。GCD 需要和 blocks(Objective-C 的闭包)配合使用。block 是 GCD 执行单元。GCD 的任务需要被拆解到 block 中。block 被排入 GCD 的分发队列,GCD 会为你排期运行。GCD 创建,重用,销毁线程,基于系统资源以它认为合适的方式运行每个队列。所以,用户需要关心的细节并不多。
GCD 的使用也很简单,假设抢面包是个耗时操作,前面例子中的 Geroge 和 Totty 的工作都可以实现如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 并发队列中做耗时操作
while (TRUE) {
if(_cake > 0) {
// 耗时操作
[NSThread sleepForTimeInterval:0.5];
_cake--;
_occupied = kSum - _cake;
} else {
break;
}
}
// 主队列中刷新界面
dispatch_async(dispatch_get_main_queue(), ^{
[self updateUI];
});
});
GCD 分发队列
使用分发队列,你可以异步或者阻塞执行任意多个 block 的代码。你可以使用分发队列来执行几乎任何线程任务。
在 GCD 中存在三种队列:
串行分发队列(Serial dispatch queue)
串行分发队列又被称为私有分发队列,按顺序执行队列中的任务,且同一时间只执行一个任务。串行分发队列常用于实现同步锁。下面代码创建了一个串行分发队列:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.MyQueue", NULL);
并发分发队列(Concurrent dispatch queue)
串行分发队列又被称为全局分发队列,也按顺序执行队列中的任务,但是顺序开始的多个任务会并发同时执行。并发分发队列常用于管理并发任务。下面代码创建了一个并发分发队列:
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
主分发队列(Main dispatch queue)
主分发队列是一个全局唯一的特殊的串行分发队列。队列中的任务会被在应用的主线程中执行。主分发队列可以用于执行 UI 相关的操作。取得主分发队列的方法:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
GCD 任务执行方式
GCD 中有两种任务执行方式:
- 异步执行, dispatch_async,意味将任务放入队列之后,主线程不会等待 block 的返回结果,而是立即继续执行下去。
- 阻塞执行, dispatch_sync,意味将任务放入队列之后,主线程被阻塞,需要等待 block 的执行结果返回,才能继续执行下去。
GCD 的其他主题
GCD 有着丰富的功能,比如分发组(dispatch group),信号(semaphores),分发栅栏(dispatch barrier),分发源(dispatch source)等等。这些可以用于完成更复杂的多线程任务。详细可以查阅 Apple 关于 GCD 的文档。
Apple 公司宣称其在 GCD 技术中为更好地利用多核硬件系统做了很多的优化。所以,在性能方面 GCD 是不用担心的。而且 GCD 也提供了相当丰富的 API,几乎可以完成绝大部分线程相关的编程任务。所以,在多线程相关主题的编程中,GCD 应该是首选。
GCD使用例子
使用 GCD 的 dispatch queue 实现同步锁
同步锁的实现方案有不少,比如,如果仅仅是想对某个实例变量的读写操作加锁,可以使用属性(property)的 atomic 参数,对于一段代码加锁可以使用 @synchronized 块,或者 NSLock。
// Method 1
- (void)synchronizedMethod {
@synchronized(self) {
// safe
}
}
// Method 2
_lock = [[NSLock alloc] init];
- (void)synchronizedMethod {
[_lock lock];
// Safe
[_lock unlock];
}
@synchronized 一般会以 self 为同步对象。重复调用 @synchronized(self) 是很危险的。如果多个属性这么做,每一个属性将会被和其它所有属性同步,这可能并不是你所希望的,更好的方法是每个属性的锁都是相互独立的。
另一种方法是使用 NSLock 实现同步锁,这个方法不错,但是缺点是在极端环境下同步块可能会导致锁死,而且这种情况下处理锁死状态会有麻烦。
一个替代方法是使用 GCD 的分发队列。将读和写分发到相同并发队列中,这样读操作会是并发的,多个线程可以同时执行写操作;而对于写操作,以分发栅栏(dispatch barrier)保证同时只有一个线程可以执行写操作,并且由于写操作无需返回,写操作还是异步马上返回的。这样,就得到了一个高效且线程安全的锁。代码看起来会像这样:
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSInteger)cake {
__block NSInteger localCake;
dispatch_sync(_syncQueue, ^{
localCake = _cake;
});
return localCake;
}
- (void)setCake:(NSInteger)cake {
dispatch_barrier_async(_syncQueue, ^{
_cake = cake;
});
}
简单而言,上面的代码可以使读操作被竞争执行;写操作被互斥执行,并且异步返回。使用 GCD 实现的这个同步锁应该是效率最优且最安全的。
使用 GCD 替代 performSelector 系列方法
NSObject 的 performSelector 系列方法有很多限制。传给要执行的方法的参数的数量是有限制的,也没法方法保证能正确地取得要执行的方法的返回值。这些限制在使用 block 的 GCD 中都不存在。
使用 GCD 替代 performSelector 的例子。
// 使用 performSelector 系列方法:
[self performSelector:@selector(cake)
withObject:nil
afterDelay:5.0];
// 使用 GCD 完成相同的事情:
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
[self cake];
});
使用 dispatch_once 实现线程安全单一执行要求
线程安全单一执行典型例子是单例,GCD 的 dispatch_once 能够保证传入的 block 被线程安全地唯一执行:
+ (id)sharedInstance {
static AdivseDemoController *sharedInstance = nil;
static dispatch_once_t onceToken = @"token";
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
当需要更细粒度控制线程时,考虑 NSOperation
以下是你需要考虑使用 NSOperation 的一些理由:
- 当你需要取消线程任务时,GCD 无法提供取消任务的操作。而 NSOperation 提供了取消任务的操作;
- 当你需要更细的粒度地观察任务改变了状态时,由于 NSOperation 是一个对象,比较 GCD 使用的 block 而言,通过对 NSOperation 对象进行键值观察(KVO)能很容易观察到任务的状态改变;
- 当你需要重用线程任务时,NSOperation 作为一个普通的 Objective-C 对象,可以存储任何信息。对象就是为重用而设计的,这时,NSOperation 比 GCD 使用的 block 要更方便。
POSIX 线程API
OS X 和 iOS 是多线程操作系统,它们追随 UNIX 系统使用了 POSIX 线程模型。OS X 和 iOS 都提供了一套底层的 C 语言 POSIX 线程 API 来创建和管理线程。
实际应用开发中,除非需要跨平台,应用层并不常直接使用 POSIX 线程API <pthread.h>
。
参考
- https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/ConcurrencyandApplicationDesign/ConcurrencyandApplicationDesign.html
- https://developer.apple.com/reference/dispatch?language=objc
- http://www.infoq.com/cn/articles/os-x-ios-multithread-technology
- http://www.jianshu.com/p/6e6f4e005a0b
- http://www.jianshu.com/p/0b0d9b1f1f19
- https://www.google.com/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=ios%20%E5%A4%9A%E7%BA%BF%E7%A8%8B