本文整理了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 中有两种任务执行方式:

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 的一些理由:

POSIX 线程API

OS X 和 iOS 是多线程操作系统,它们追随 UNIX 系统使用了 POSIX 线程模型。OS X 和 iOS 都提供了一套底层的 C 语言 POSIX 线程 API 来创建和管理线程。

实际应用开发中,除非需要跨平台,应用层并不常直接使用 POSIX 线程API <pthread.h>

参考