在单核 CPU 时代,支持多线程的操作系统会通过分配 CPU 计算时间,来实现软件层面的多线程。创建线程,线程间切换都是有成本开销的。但由于多线程可以避免阻塞所造成的 CPU 计算时间浪费,所以多线程所带来的开销成本总体看来是值得的。任务一般都可以被拆分成多个子任务,如果一个子任务发生了阻塞,计算时间就可以分配给其他子任务。这样就提高了 CPU 的利用率。
在多核 CPU 时代,就更好理解了。由于硬件上就支持多线程技术,就可以让多个线程真正同时地运行。如果任务能够被拆分,各个子任务就能并行地在 CPU 上运行,这就能显著加快运行速度。
总结说来,多线程的目的是,通过并发执行提高 CPU 的使用效率,进而提供程序运行效率。
OS X 和 iOS 是多线程操作系统,它们追随 UNIX 系统使用了 POSIX 线程模型。OS X 和 iOS 都提供了一套底层的 C 语言 POSIX 线程 API 来创建和管理线程。但实际应用开发中,除非需要跨平台,我们并不常直接使用 POSIX 线程 API,而是使用系统或语言提供的其他一些更为简单的方案,下一节中会讨论它们。总结说来,多线程的目的是,通过并发执行提高 CPU 的使用效率,进而提供程序运行效率。
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
这一系列方法简单易用,但只提供了有限的几个选择:指定执行的方法(但传入方法的参数数量有限制);指定是在当前线程,还是在主线程,还是在后台线程执行;指定是否需要阻塞当前线程等待结果。
例如,以下代码使得方法 foo: 在一个新的后台线程执行,并传入了 object 参数:
SEL selector = @selector(foo:); [self performSelectorInBackground:selector withObject:object];
以下代码使得 updateUI 方法在主线程内得到执行,并且当前线程会被阻塞,直到主线程执行完该函数:
[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:YES];
NSThread 是 OS X 和 iOS 都提供的一个线程对象,它是线程的一个轻量级实现。在执行一些轻量级的简单任务时,NSThread 很有用,但用户仍然需要自己管理线程生命周期,进行线程间同步。比如,线程状态,依赖性,线程间同步等线程相关的主题 NSThread 都没有涉及。比如,涉及到线程间同步仍然需要配合使用 NSLock,NSCondition 或者 @synchronized。所以,遇到复杂任务时,轻量级的 NSThread 可能并不合适。
提供一个模拟多线程运作的简单例子:两个人同时一起到烤箱抢面包。我们启动两个线程,来代表两个人。由于烤箱门比较小,同时只能有一个人去拿面包。由于 NSThread 不处理线程同步,所以为了模拟这个过程, 你还需要一把线程锁(即类型为 NSLock 的实例变量 _lock)。在后面的 run 方法中会用到这把线程锁:
_lock = [[NSLock alloc] init]; NSThread *geroge = [[NSThread alloc] itWithTarget:self selector: @selector(run) object:nil]; [geroge setName:@"Geroge"]; [geroge start]; NSThread *totty = [[NSThread alloc] nitWithTarget:self selector: @selector(run) object:nil]; [totty setName:@"Totty"]; [totty start];
受到线程锁保护的拿面包过程可以用下面的 run 方法表示:
- (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]; } }
NSOperation 做的事情比 NSThread 更多一些。通过继承 NSOperation,可以使子类获得一些线程相关的特性,进而可以安全地管理线程生命周期。比如,以线程安全的方式建立状态,取消线程。配合 NSOperationQueue,可以控制线程间的优先级和依赖性。这就给出了一套线程管理的基本方法。
NSOperation 代表了一个独立的计算单元。一般,我们会把计算任务封装进 NSOperation 这个对象。NSOperation 是抽象类,但同时也提供了两个可以直接使用的实体子类:NSInvocationOperation 和 NSBlockOperation。NSInvocationOperation 用于将计算任务封装进方法,NSBlockOperation 用于将计算任务封装进 block。
NSOperationQueue 则用于执行计算任务,管理计算任务的优先级,处理计算任务之间的依赖性。NSOperation 被添加到 NSOperationQueue 中之后,队列会按优先级和进入顺序调度任务,NSOperation 对象会被自动执行。
仍然使用上一节 NSThread 中的模拟两人抢面包的例子。由于计算任务没有变化,所以 run 方法并不改变。但这里需要使用 NSOperation 和 NSOperationQueue 来代表两个抢面包的人,并给予他们不同的优先级。由于 NSOperation 也不处理线程间同步问题,所以你仍然需要一把在 run 方法中会用到的线程锁:
_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];
NSOperation 提供以下任务优先级,以这些优先级设置变量 queuePriority 即可加快或者推迟操作的执行:
NSOperation 使用状态机模型来表示状态。通常,你可以使用 KVO(Key-Value Observing)观察任务的执行状态。这是其他多线程工具所不具备的功能。NSOperation 提供以下状态:
NSOperation 对象之间的依赖性可以用如下代码表示:
[refreshUIOperation addDependency:requestDataOperation]; [operationQueue addOperation:requestDataOperation]; [operationQueue addOperation:refreshUIOperation];
除非 requestDataOperation 的状态 isFinished 返回 YES,不然 refreshUIOperation 这个操作不会开始。
NSOperation 还有一个非常有用功能,就是“取消”。这是其他多线程工具(包括后面要讲到的 GCD)都没有的。调用 NSOperation 的 cancel: 方法即可取消该任务。当你知道这个任务没有必要再执行下去时,尽早安全地取消它将有利于节省系统资源。
GCD(Grand Central Dispatch)是 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 提供了简单易用的接口。
在 GCD 中存在三种队列:
串行分发队列又被称为私有分发队列,按顺序执行队列中的任务,且同一时间只执行一个任务。串行分发队列常用于实现同步锁。下面代码创建了一个串行分发队列:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.MyQueue", NULL);
串行分发队列又被称为全局分发队列,也按顺序执行队列中的任务,但是顺序开始的多个任务会并发同时执行。并发分发队列常用于管理并发任务。下面代码创建了一个并发分发队列:
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
主分发队列是一个全局唯一的特殊的串行分发队列。队列中的任务会被在应用的主线程中执行。主分发队列可以用于执行 UI 相关的操作。取得主分发队列的方法:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
GCD 中有两种任务执行方式:
GCD 有着丰富的功能,比如分发组(dispatch group),信号(semaphores),分发栅栏(dispatch barrier),分发源(dispatch source)等等。这些可以用于完成更复杂的多线程任务。详细可以查阅 Apple 关于 GCD 的文档。
Apple 公司宣称其在 GCD 技术中为更好地利用多核硬件系统做了很多的优化。所以,在性能方面 GCD 是不用担心的。而且 GCD 也提供了相当丰富的 API,几乎可以完成绝大部分线程相关的编程任务。所以,在多线程相关主题的编程中,GCD 应该是首选。下面举一些可以推荐使用 GCD 的实际例子:
同步锁的实现方案有不少,比如,如果仅仅是想对某个实例变量的读写操作加锁,可以使用属性(property)的 atomic 参数,对于一段代码加锁可以使用 @synchronized 块,或者 NSLock。
@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 实现的这个同步锁应该是效率最优且最安全的。
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]; });
线程安全单一执行典型例子是单例,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; }
这是现在 Objective-C 中实现单例较为推荐的一种方法。
GCD 虽然在很多地方值得提倡,但并不是任务管理和多线程地唯一解决方案,并不是说所有的地方都应该使用 GCD。GCD 是一个纯 C API,NSOperation 是 Objective-C 类,在一些地方对象编程是有优势的。NSOperation 也提供了一些 GCD 无法实现,或者 GCD 所没有的功能。
以下是你需要考虑使用 NSOperation 的一些理由:
OS X 和 iOS 系统提供了丰富的多线程工具。这些工具中,最新最现代的是 GCD(Grand Central Dispatch)。GCD 也是 Apple 公司推荐的多线程解决方案。所以,对于多线程技术的选择,总结下来有这两条建议: