OC中并发编程的相关API和面临的挑战

原文:http://beyondvincent.com/


小引

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。

 
 
  1. struct threadInfo {
  2. uint32_t * inputValues;
  3. size_t count;
  4. };
  5.  
  6. struct threadResult {
  7. uint32_t min;
  8. uint32_t max;
  9. };
  10.  
  11. void * findMinAndMax(void *arg)
  12. {
  13. struct threadInfo const * const info = (struct threadInfo *) arg;
  14. uint32_t min = UINT32_MAX;
  15. uint32_t max = 0;
  16. for (size_t i = 0; i < info>count; ++i) {
  17. uint32_t v = info>inputValues[i];
  18. min = MIN(min, v);
  19. max = MAX(max, v);
  20. }
  21. free(arg);
  22. struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));
  23. result>min = min;
  24. result>max = max;
  25. return result;
  26. }
  27.  
  28. int main(int argc, const char * argv[])
  29. {
  30. size_t const count = 1000000;
  31. uint32_t inputValues[count];
  32.  
  33. // Fill input values with random numbers:
  34. for (size_t i = 0; i < count; ++i) {
  35. inputValues[i] = arc4random();
  36. }
  37.  
  38. // Spawn 4 threads to find the minimum and maximum:
  39. size_t const threadCount = 4;
  40. pthread_t tid[threadCount];
  41. for (size_t i = 0; i < threadCount; ++i) { struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info)); size_t offset = (count / threadCount) * i; info>inputValues = inputValues + offset;
  42. info>count = MIN(count - offset, count / threadCount);
  43. int err = pthread_create(tid + i, NULL, &findMinAndMax, info);
  44. NSCAssert(err == 0, @"pthread_create() failed: %d", err);
  45. }
  46. // Wait for the threads to exit:
  47. struct threadResult * results[threadCount];
  48. for (size_t i = 0; i < threadCount; ++i) {
  49. int err = pthread_join(tid[i], (void **) &(results[i]));
  50. NSCAssert(err == 0, @"pthread_join() failed: %d", err);
  51. }
  52. // Find the min and max:
  53. uint32_t min = UINT32_MAX;
  54. uint32_t max = 0;
  55. for (size_t i = 0; i < threadCount; ++i) { min = MIN(min, results[i]>min);
  56. max = MAX(max, results[i]>max);
  57. free(results[i]);
  58. results[i] = NULL;
  59. }
  60.  
  61. NSLog(@"min = %u", min);
  62. NSLog(@"max = %u", max);
  63. return 0;
  64. }

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

 
 
  1. @interface FindMinMaxThread : NSThread
  2. @property (nonatomic) NSUInteger min;
  3. @property (nonatomic) NSUInteger max;
  4. - (instancetype)initWithNumbers:(NSArray *)numbers;
  5. @end
  6.  
  7. @implementation FindMinMaxThread {
  8. NSArray *_numbers;
  9. }
  10.  
  11. - (instancetype)initWithNumbers:(NSArray *)numbers
  12. {
  13. self = [super init];
  14. if (self) {
  15. _numbers = numbers;
  16. }
  17. return self;
  18. }
  19.  
  20. - (void)main
  21. {
  22. NSUInteger min;
  23. NSUInteger max;
  24. // process the data
  25. self.min = min;
  26. self.max = max;
  27. }
  28. @end

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

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

现在,当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返回的时候,就可以假定操作结束。

 
 
  1. @implementation YourOperation
  2. - (void)main
  3. {
  4. // do your work here ...
  5. }
  6. @end

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

 
 
  1. @implementation YourOperation
  2. - (void)start
  3. {
  4. self.isExecuting = YES;
  5. self.isFinished = NO;
  6. // start your work, which calls finished once it's done ...
  7. }
  8.  
  9. - (void)finished
  10. {
  11. self.isExecuting = NO;
  12. self.isFinished = YES;
  13. }
  14. @end

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

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

 
 
  1. - (void)main
  2. {
  3. while (notDone && !self.isCancelled) {
  4. // do your processing
  5. }
  6. }

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

 
 
  1. NSOperationQueue *queue = [[NSOperationQueue alloc] init];
  2. YourOperation *operation = [[YourOperation alloc] init];
  3. [queue addOperation:operation];

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

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

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

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

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

 
 
  1. [intermediateOperation addDependency:operation1];
  2. [intermediateOperation addDependency:operation2];
  3. [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,都会立即退出。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值