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

小引

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。 本文会介绍 pthread和NSThread、Grand 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.     struct threadInfo const * const info = (struct threadInfo *) arg; 
  13.     uint32_t min = UINT32_MAX; 
  14.     uint32_t max = 0; 
  15.     for (size_t i = 0; i < info-&gt;count; ++i) { 
  16.         uint32_t v = info-&gt;inputValues[i]; 
  17.         min = MIN(min, v); 
  18.         max = MAX(max, v); 
  19.     } 
  20.     free(arg); 
  21.     struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result)); 
  22.     result-&gt;min = min; 
  23.     result-&gt;max = max; 
  24.     return result; 
  25.   
  26. int main(int argc, const char * argv[]) 
  27.     size_t const count = 1000000; 
  28.     uint32_t inputValues[count]; 
  29.   
  30.     // Fill input values with random numbers: 
  31.     for (size_t i = 0; i < count; ++i) { 
  32.         inputValues[i] = arc4random(); 
  33.     } 
  34.   
  35.     // Spawn 4 threads to find the minimum and maximum: 
  36.     size_t const threadCount = 4; 
  37.     pthread_t tid[threadCount]; 
  38.     for (size_t i = 0; i < threadCount; ++i) {         struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info));         size_t offset = (count / threadCount) * i;         info-&gt;inputValues = inputValues + offset; 
  39.         info-&gt;count = MIN(count - offset, count / threadCount); 
  40.         int err = pthread_create(tid + i, NULL, &amp;findMinAndMax, info); 
  41.         NSCAssert(err == 0, @"pthread_create() failed: %d", err); 
  42.     } 
  43.     // Wait for the threads to exit: 
  44.     struct threadResult * results[threadCount]; 
  45.     for (size_t i = 0; i < threadCount; ++i) { 
  46.         int err = pthread_join(tid[i], (void **) &amp;(results[i])); 
  47.         NSCAssert(err == 0, @"pthread_join() failed: %d", err); 
  48.     } 
  49.     // Find the min and max: 
  50.     uint32_t min = UINT32_MAX; 
  51.     uint32_t max = 0; 
  52.     for (size_t i = 0; i < threadCount; ++i) {         min = MIN(min, results[i]-&gt;min); 
  53.         max = MAX(max, results[i]-&gt;max); 
  54.         free(results[i]); 
  55.         results[i] = NULL; 
  56.     } 
  57.   
  58.     NSLog(@"min = %u", min); 
  59.     NSLog(@"max = %u", max); 
 
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. - (instancetype)initWithNumbers:(NSArray *)numbers  
  11.     self = [super init]; 
  12.     if (self) { 
  13.         _numbers = numbers; 
  14.     } 
  15.     return self; 
  16.   
  17. - (void)main 
  18.     NSUInteger min; 
  19.     NSUInteger max; 
  20.     // process the data 
  21.     self.min = min; 
  22.     self.max = max; 
  23. @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]; 
 现在,当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是稍微偏底层的一个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.     while (notDone &amp;&amp; !self.isCancelled) { 
  3.         // do your processing 
  4.     } 
 
当开发者定义好操作类之后,就可以很容易的将一个操作添加到队列中:
 
 
  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、付费专栏及课程。

余额充值