小引

http://www.objc.io/站点主要以杂志的形式,深入挖掘在OC中的最佳编程实践和高级技术,每个月探讨一个主题,每个主题都会有几篇相关的文章出炉,2013年7月份的主题是并发编程,今天晚上我就挑选其中的第2篇文章(Concurrent Programming: APIs and Challenges)进行翻译,与大家分享一下主要内容。由于内容比较多,我将分两部分翻译(API和难点)完成,翻译中,如有错误,还请指正。

目录

1、介绍
2、OS X和iOS中的并发编程
    2.1、Threads
   2.2、Grand Central Dispatch
   2.3、Operation Queues
   2.4、Run Loops
3、并发编程中面临的挑战
   3.1、资源共享
   3.2、互斥
   3.3、死锁
   3.4、饥饿
   3.5、优先级反转
4、小结

正文

1、介绍

 并发的意思就是同时运行多个任务,这些任务可以在单核CPU上以分时(时间共享)的形式同时运行,或者在多核CPU上以真正的并行来运行多任务。

OS X和iOS提供了几种不同的API来支持并发编程。每种API都具有不同的功能和一些限制,一般是根据不同的任务使用不同的API。这些API在系统中处于不同的地方。并发编程对于开发者来说非常的强大,但是作为开发者需要担负很大的责任,来把任务处理好。

实际上,并发编程是一个很有挑战的主题,它有许多错综复杂的问题和陷阱,当开发者在使用类似GCDNSOperationQueue API时,很容易遗忘这些问题和陷阱。本文首先介绍一下OS X和iOS中不同的并发编程API,然后深入了解并发编程中开发者需要面临的一些挑战。

2、OS X和iOS中的并发编程

在移动和桌面操作系统中,苹果提供了相同的并发编程API。 本文会介绍pthreadNSThreadGrand Central Dispatch(GCD)NSOperationQueue,以及NSRunLoop。NSRunLoop列在其中,有点奇怪,因为它并没有被用来实现真正的并发,不过NSRunLoop与并发编程有莫大的关系,值得我们去了解。

由于高层API是基于底层API构建的,所以首先将从底层的API开始介绍,然后逐步介绍高层API,不过在具体编程中,选择API的顺序刚好相反:因为大多数情况下,选择高层的API不仅可以完成底层API能完成的任务,而且能够让并发模型变得简单。

如果你对这里给出的建议(API的选择)上有所顾虑,那么你可以看看本文的相关内容:并发编程面临的挑战,以及Peter Steinberger写的关于线程安全的文章。

2.1、Threads

线程(thread)是组成进程的子单元,操作系统的调度器可以对线程进行单独的调度。实际上,所有的并发编程API都是构建于线程之上的——包括GCD和操作队列(operation queues)。

多线程可以在单核CPU上同时运行(可以理解为同一时间)——操作系统将时间片分配给每一个线程,这样就能够让用户感觉到有多个任务在同时进行。如果CPU是多核的,那么线程就可以真正的以并发方式被执行,所以完成某项操作,需要的总时间更少。

开发者可以通过Instrument中的CPU strategy view来观察代码被执行时在多核CPU中的调度情况。

需要重点关注的一件事:开发者无法控制代码在什么地方以及什么时候被调度,以及无法控制代码执行多长时间后将被暂停,以便轮到执行别的任务。线程调度是非常强大的一种技术,但是也非常复杂(稍后会看到)。

先把线程调度的复杂情况放一边,开发者可以使用POSIX线程API,或者Objective-C中提供的对该API的封装——NSThread,来创建自己的线程。下面这个小示例是利用pthread来查找在一百万个数字中的最小值和最大值。其中并发执行了4个线程。从该示例复杂的代码中,可以看出为什么我们不希望直接使用pthread。

struct threadInfo {    uint32_t * inputValues;    size_t count;}; struct threadResult {    uint32_t min;    uint32_t max;}; void * findMinAndMax(void *arg){    struct threadInfo const * const info = (struct threadInfo *) arg;    uint32_t min = UINT32_MAX;    uint32_t max = 0;    for (size_t i = 0; i count; ++i) {        uint32_t v = info>inputValues[i];        min = MIN(min, v);        max = MAX(max, v);    }    free(arg);    struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));    result>min = min;    result>max = max;    return result;} int main(int argc, const char * argv[]){    size_t const count = 1000000;    uint32_t inputValues[count];     // Fill input values with random numbers:    for (size_t i = 0; i < count; ++i) {        inputValues[i] = arc4random();    }     // Spawn 4 threads to find the minimum and maximum:    size_t const threadCount = 4;    pthread_t tid[threadCount];    for (size_t i = 0; i inputValues = inputValues + offset;        info>count = MIN(count - offset, count / threadCount);        int err = pthread_create(tid + i, NULL, &findMinAndMax, info);        NSCAssert(err == 0, @"pthread_create() failed: %d", err);    }    // Wait for the threads to exit:    struct threadResult * results[threadCount];    for (size_t i = 0; i < threadCount; ++i) {        int err = pthread_join(tid[i], (void **) &(results[i]));        NSCAssert(err == 0, @"pthread_join() failed: %d", err);    }    // Find the min and max:    uint32_t min = UINT32_MAX;    uint32_t max = 0;    for (size_t i = 0; i min);        max = MAX(max, results[i]>max);        free(results[i]);        results[i] = NULL;    }     NSLog(@"min = %u", min);    NSLog(@"max = %u", max);    return 0;}

NSThread是Objective-C对pthread的一个封装。通过封装,在Cocoa环境中,可以让代码看起来更加亲切。例如,开发者可以利用NSThread的一个子类来定义一个线程,在这个子类的中封装了需要运行的代码。针对上面的那个例子,我们可以定义一个这样的NSThread子类:

@interface FindMinMaxThread : NSThread@property (nonatomic) NSUInteger min;@property (nonatomic) NSUInteger max;- (instancetype)initWithNumbers:(NSArray *)numbers;@end @implementation FindMinMaxThread {    NSArray *_numbers;} - (instancetype)initWithNumbers:(NSArray *)numbers {    self = [super init];    if (self) {        _numbers = numbers;    }    return self;} - (void)main{    NSUInteger min;    NSUInteger max;    // process the data    self.min = min;    self.max = max;}@end

要想启动一个新的线程,需要创建一个线程对象,然后调用它的start方法:

NSSet *threads = [NSMutableSet set];NSUInteger numberCount = self.numbers.count;NSUInteger threadCount = 4;for (NSUInteger i = 0; i < threadCount; i++) {    NSUInteger offset = (count / threadCount) * i;    NSUInteger count = MIN(numberCount - offset, numberCount / threadCount);    NSRange range = NSMakeRange(offset, count);    NSArray *subset = [self.numbers subarrayWithRange:range];    FindMinMaxThread *thread = [[FindMinMaxThread alloc] initWithNumbers:subset];    [threads addObject:thread];    [thread start];}

现在,当4个线程结束的时候,我们检测到线程的isFinished属性。不过最好还是远离上面的代码吧——最主要的原因是,在编程中,直接使用线程(无论是pthread,还是NSThread)都是难以接受的。

使用线程会引发的一个问题就是:在开发者自己的代码,或者系统内部的框架代码中,被激活的线程数量很有可能会成倍的增加——这对于一个大型工程来说,是很常见的。例如,在8核CPU中,你创建了8个线程,然后在这些线程中调用了框架代码,这些代码也创建了同样的线程(其实它并不知道你已经创建好线程了),这样会很快产生成千上万个线程,最终导致你的程序被终止执行——线程实际上并不是免费的咖啡,每个线程的创建都会消耗一些内容,以及相关的内核资源。

下面,我将介绍两个基于队列的并发编程API:GCD和operation queue。它们通过集中管理一个线程池(被没一个任务协同使用),来解决上面遇到的问题。

2.2、Grand Central Dispatch

为了让开发者更加容易的使用设备上的多核CPU,苹果在OS X和iOS 4中引入了Grand Central Dispatch(GCD)。在下一篇文章中会更加详细的介绍GCD:low-level concurrency APIs

通过GCD,开发者不用再直接跟线程打交道了,只需要向队列中添加block代码即可,GCD在后端管理着一个线程池。GCD不仅决定着哪个线程(block)将被执行,它还根据可用的系统资源对线程池中的线程进行管理——这样可以不通过开发者来集中管理线程,缓解大量线程的创建,做到了让开发者远离线程的管理。

默认情况下,GCD公开有5个不同的队列:运行在主线程中的main queue,3个不同优先级的后台队列,以及一个优先级更低的后台队列(用于I/O)。另外,开发者可以创建自定义队列:串行或者并行队列。自定义队列非常强大,在自定义队列中被调度的所有block都将被放入到系统的线程池的一个全局队列中。

gcd-queues@2x

这里队列中,可以使用不同优先级,这听起来可能非常简单,不过,强烈建议,在大多数情况下使用默认的优先级就可以了。在队列中调度具有不同优先级的任务时,如果这些任务需要访问一些共享的资源,可能会迅速引起不可预料到的行为,这样可能会引起程序的突然停止——运行时,低优先级的任务阻塞了高优先级任务。更多相关内容,在本文的优先级反转中会有介绍。

虽然GCD是稍微偏底层的一个API,但是使用起来非常的简单。不过这也容易使开发者忘记并发编程中的许多注意事项和陷阱。读者可以阅读本文后面的:并发编程中面临的挑战,这样可以注意到一些潜在的问题。本期的另外一篇文章:Low-level Concurrency API,给出了更加深入的解释,以及一些有价值的提示。

2.3、Operation Queues

操作队列(operation queue)是基于GCD封装的一个队列模型。GCD提供了更加底层的控制,而操作队列在GCD之上实现了一些方便的功能,这些功能对于开发者来说会更好、更安全。

类NSOperationQueue有两个不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。任何情况下,在这两种队列中运行的任务,都是由NSOperation组成。

定义自己的操作有两种方式:重写main或者start方法,前一种方法非常简单,但是灵活性不如后一种。对于重写main方法来说,开发者不需要管理一些状态属性(例如isExecuting和isFinished)——当main返回的时候,就可以假定操作结束。

@implementation YourOperation    - (void)main    {        // do your work here ...    } @end

如果你希望拥有更多的控制权,以及在一个操作中可以执行异步任务,那么就重写start方法:

@implementation YourOperation    - (void)start    {        self.isExecuting = YES;        self.isFinished = NO;        // start your work, which calls finished once it's done ...    }     - (void)finished    {        self.isExecuting = NO;        self.isFinished = YES;    }@end

注意:这种情况下,需要开发者手动管理操作的状态。 为了让操作队列能够捕获到操作的改变,需要将状态属性以KVO的方式实现。并确保状态改变的时候发送了KVO消息。

为了满足操作队列提供的取消功能,还应该检查isCancelled属性,以判断是否继续运行。

- (void)main{    while (notDone && !self.isCancelled) {        // do your processing    }}

当开发者定义好操作类之后,就可以很容易的将一个操作添加到队列中:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];YourOperation *operation = [[YourOperation alloc] init];[queue  addOperation:operation];

另外,开发者也可以将block添加到队列中。这非常的方便,例如,你希望在主队列中调度一个一次性任务:

[[NSOperationQueue mainQueue] addOperationWithBlock:^{    // do something...}];

如果重写operation的description方法,可以很容易的标示出在某个队列中当前被调度的所有operation。

除了提供基本的调度操作或block外,操作队列还提供了一些正确使用GCD的功能。例如,可以通过maxConcurrentOperationCount属性来控制一个队列中可以有多少个操作参与并发执行,以及将队列设置为一个串行队列。

另外还有一个方便的功能就是根据队列中operation的优先级对其进行排序,这不同于GCD的队列优先级,它只会影响到一个队列中所有被调度的operation的执行顺序。如果你需要进一步控制operation的执行顺序(除了使用5个标准的优先级),还可以在operation之间指定依赖,如下:

[intermediateOperation addDependency:operation1];[intermediateOperation addDependency:operation2];[finishedOperation addDependency:intermediateOperation];

上面的代码可以确保operation1和operation在intermediateOperation之前执行,也就是说,在finishOperation之前被执行。对于需要明确的执行顺序时,操作依赖是非常强大的一个机制。 它可以让你创建一些操作组,并确保这些操作组在所依赖的操作之前被执行,或者在并发队列中以串行的方式执行operation。

从本质上来看,操作队列的性能比GCD要低,不过,大多数情况下,可以忽略不计,所以操作队列是并发编程的首选API。

2.4、Run Loops

实际上,Run loop并不是一项并发机制(例如GCD或操作队列),因为它并不能并行执行任务。不过在主dispatch/operation队列中,run loop直接配合着任务的执行,它提供了让代码异步执行的一种机制。

Run loop比起操作队列或者GCD来说,更加容易使用,因为通过run loop,开发者不必处理并发中的复杂情况,就能异步的执行任务。

一个run loop总是绑定到某个特定的线程中。main run loop是与主线程相关的,在每一个Cocoa和CocoaTouch程序中,这个main run loop起到核心作用——它负责处理UI时间、计时器,以及其它内核相关事件。无论什么时候使用计时器、NSURLConnection或者调用performSelector:withObject:afterDelay:,run loop都将在后台发挥重要作用——异步任务的执行。

无论什么时候,依赖于run loop使用一个方法,都需要记住一点:run loop可以运行在不同的模式中,每种模式都定义了一组事件,供run loop做出响应——这其实是非常聪明的一种做法:在main run loop中临时处理某些任务。

在iOS中非常典型的一个示例就是滚动,在进行滚动时,run loop并不是运行在默认模式中的,因此,run loop此时并不会做出别的响应,例如,滚动之前在调度一个计时器。一旦滚动停止了,run loop会回到默认模式,并执行添加到队列中的相关事件。如果在滚动时,希望计时器能被触发,需要将其在NSRunLoopCommonModes模式下添加到run loop中。

其实,默认情况下,主线程中总是有一个run loop在运行着,而其它的线程默认情况下,不会有run loop。开发者可以自行为其它的线程添加run loop,只不过很少需要这样做。大多数时候,使用main run loop更加方便。如果有大量的任务不希望在主线程中执行,你可以将其派发到别的队列中。相关内容,Chris写了一篇文章,可以去看看:common background practices

如果你真需要在别的线程中添加一个run loop,那么不要忘记在run loop中至少添加一个input source。如果run loop中没有input source,那么每次运行这个run loop,都会立即退出。