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

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,都会立即退出。
 
关于并发编程中面临的挑战,会在下一篇文章中出现。
 
 

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

小引
上一篇文章介绍了OC中并发编程的相关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、小结
 
正文
 
3、并发编程中面临的挑战
使用并发编程会带来许多陷进。尽管开发者做得足够到位了,还是难以观察并行执行中相互作用的多任务的不同状态。问题往往发生在一些不确定性(不可预见性)的地方,在调试相关并发代码时会感觉到很无助。
 
关于并发编程的不可预见性有一个非常典型的例子:在1995年,NASA(美国宇航局)发送了火星探测器,但是当探测器成功着陆的时候,任务嘎 然而止,火星探测器莫名其妙的不停重启——在计算机领域内,遇到的这中现象被定为为优先级反转,也就是说低优先级的线程一直阻塞着高优先级的线程。稍后我 们会看到更多相关介绍。通过该示例,可以告诉我们即使拥有丰富的资源和大量优秀工程师,但是也会遭遇使用并发编程带来的陷阱。
 
3.1、资源共享
并发编程中许多问题的根源就是在多线程中访问共享资源。资源可以是一个属性、一个对象,通用的内存、网络设备和文件等等。在多线程中任意共享的资源都有一个潜在的冲突,开发者必须防止相关冲突的发生。
 
为了演示冲突问题,我们来看一个关于资源的简单示例:利用一个整型值作为计数器。在程序运行过程中,有两个并行线程A和B,这两个线程都尝试着 同时增加计数器的值。问题来了,通过C或OC写的代码(增加计数器的值)不仅仅是一条指令,而是包括好多指令——要想增加计数器的值,需要从内存中读取出 当前值,然后再增加计数器的值,最后还需要就爱那个这个增加的值写回内存中。
 
我们可以试着想一下,如果两个线程同时做上面涉及到的操作,会发生什么问题。例如,线程A和B都从内存中读取出了计数器的值,假设为17,然后 线程A将计数器的值加1,并将结果18写回到内存中。同时,线程B也将计数器的值加1,并将结果18写回到内存中。实际上,此时计数器的值已经被破坏掉了 ——因为计数器的值17被加1了两次,应该为19,但是内存中的值为18。
 
 
这个问题成为资源竞争,或者叫做race condition, 在多线程里面访问一个共享的资源,如果没有一种机制来确保线程A结束访问一个共享资源之前,线程B就开始访问该共享资源,那么资源竞争的问题总是会发生。 试想一下,如果如果程序在内存中访问的资源不是一个简单的整型,而是一个复杂的数据结构,可能会发生这样的现象:当第一个线程正在读写这个数据结构时,第 二个线程也来读这个数据结构,那么获取到的数据可能是新旧参半。为了防止出现这样的问题,在多线程访问共享资源时,需要一种互斥的机制。
 
在实际的开发中,情况甚至要比上面介绍的复杂,因为现代CPU为了对代码运行达到最优化,对改变从内存中读写数据的顺序(乱序执行)。
 
3.2、互斥
互斥访问的意思就是同一时刻,只允许一个线程访问某个资源。为了保证这一点,每个希望访问共享资源的线程,首先需要获得一个共享资源的互斥锁,一旦某个线程对资源完成了读写操作,就释放掉这个互斥锁,这样别的线程就有机会访问该共享资源了。
 
除了确保互斥锁的访问,还需要解决代码无序执行所带来的问题。如果不能确保CPU访问内存的顺序跟编程时的代码指令一样,那么仅仅依靠互斥锁的访问是不够的。为了解决由CPU的优化策略引起的代码无序执行,需要引入内存屏障(memory barrier)。通过设置内存屏障,来确保无序执行时能够正确跨越设置的屏障。
 
当然,互斥锁的实现是需要自由的竞争条件。这实际上是非常重要的一个保证,并且需要在现代CPU上使用特殊的指令。更多关于原子操作(atomic operation),请阅读Daniel写的文章:底层并发技术
 
从语言层面来说,在Objective-C中将属性以atomic的形式来声明,就能支持互斥锁了。实际上,默认情况下,属性是atomic 的。将一个属性声明为atomic表示每次访问该属性都会进行加锁和解锁操作。虽然最把稳的做法就是将所有的属性都声明为atomic,但是这也会付出一 定的代价。
 
获取资源上的锁会引发一定的性能代价。获取和释放锁需要自由的竞争条件(race-condition free),这在多核系统中是很重要的。另外,在获取锁的时候,线程有时候需要等待——因为其它的线程已经获得了资源的锁。这种情况下,线程会进入休眠状 态,当其它线程释放掉相关资源的锁时,休眠的线程会得到通知。其实所有这些相关操作都是非常昂贵且复杂的。
 
这有一些不同类型的锁。当没有竞争时,有些锁是很廉价的(cheap),但是在竞争情况下,性能就会打折扣。同等条件下,另外一些锁则比较昂贵 (expensive),但是在竞争情况下,会表现更好(锁的竞争是这样产生的:当一个或者多个线程尝试获取一个已经被别的线程获取了的锁)。
 
在这里有一个东西需要进行权衡:获取和释放锁所带来的开销。开发者需要确保代码中有获取锁和释放锁的语句。同时,如果获取锁之后,要执行一大段代码,这将带来风险:其它线程可能因为资源的竞争而无法工作(需要释放掉相关的锁才行)。
 
我们经常能看到并行运行的代码,但实际上由于共享资源中配置了相关的锁,所以有时候只有一个线程是出于激活状态的。要想预测一下代码在多核上的 调度情况,有时候也显得很重要。我们可以使用Instrument的CPU strategy view来检查是否有效的利用了CPU的可用核数,进而得出更好的想法,以此来优化代码。
 
3.3、死锁
互斥解决了资源竞争的问题,但同时这也引入了一个新的问题:死锁。当多个线程在相互等待着对方的结束时,就会发生死锁,这是程序可能会被卡住。
 
 
看看下面的代码——交换两个变量的值:
  1. void swap(A, B) 
  2.     lock(lockA); 
  3.     lock(lockB); 
  4.     int a = A; 
  5.     int b = B; 
  6.     A = b; 
  7.     B = a; 
  8.     unlock(lockB); 
  9.     unlock(lockA); 
大多数时候,这能够正常运行。但是当两个线程同时调用上面这个方法呢——使用两个相反的值:
  1. swap(X, Y); // thread 1 
  2. swap(Y, X); // thread 2 
 
此时程序可能会由于死锁而被终止。线程1获得了X的一个锁,线程2获得了Y的一个锁。 接着它们会同时等待另外一把锁,但是永远都不会获得。
 
记住:在线程之间共享更多的资源,会使用更多的锁,同时也会增加死锁的概率。这也是为什么我们需要尽量减少线程间资源共享,并确保共享的资源尽量简单的原因之一。建议阅读以下底层并发编程API中的doing things asynchronously
 
3.4、饥饿
当你认为已经足够了解并发编程面临的陷阱 时,拐角处又出现了新的问题。锁定的共享资源会引起读写问题。大多数情况下,限制资源一次只能有一个线程进行访问,这是非常浪费的,比如一个读取锁只允许 读,而不对资源进行写操作,这种情况下,同时可能会有另外一个线程等着着获取一个写锁。
 
为了解决这个问题,更好的方法不是简单使用读/写锁,例如给定一个writer preference,或者使用read-copy-update算法。Daniel在底层并发技术文章中有相关介绍。
 
3.5、优先级反转
本节开头介绍了美国宇航局发射的火星探测器在火星上遇到的并发问题。现在我们就来看看为什么那个火星探测器会失败,以及为什么有时候我们的程序也会遇到相同的问题——该死的优先级反转。
 
优先级反转是指程序在运行时低优先级的任务阻塞了高优先级的任务,有效的反转了任务的优先级。由于GCD提供了后台运行队列(拥有不同的优先级),包括I/O队列,所以通过GCD我们可以很好的来了解一下优先级反转的可能性。
 
高优先级和低优先级的任务之间在共享一个资源时,就可能发生优先级反转。当低优先级的任务获得了共享资源的锁时,该任务应该迅速完成,并释放掉 锁,然后让高优先级的任务在没有明显的延时下继续执行。然而当低优先级阻塞着高优先级期间(低优先级获得的时间又比较少),如果有一个中优先级的任务(该 任务不需要那个共享资源),那么可能会抢占低优先级任务,而被执行——因为此时高优先级任务是被阻塞的,所以中优先级任务是目前所有可运行任务中优先级最 高的。此时,中优先级任务就会阻塞着低优先级任务,导致低优先级任务不能释放掉锁,也就会引起高优先级任务一直在等待锁的释放。
 
在我们的实际代码中,可能不会像火星探测器那样,遇到优先级反转时,不同的重启。
 
解决这个问题的方法,通常就是不要使用不同的优先级——将高优先级的代码和低优先级的代码修改为相同的优先级。当使用GCD时,总是使用默认的优先级队列。如果使用不同的优先级,就可能会引发事故。
 
虽然有些文章上说,在不同的队列中使用不同的优先级,这听起来不错,但是这回增加并发编程的复杂度和不可预见性。如果编程中,在高优先级任务中突然没有理由的卡住了,可能你会想起本文,以及称为优先级反转的问题,甚至还会想起美国宇航局的工程师也遇到这样的问题。
 
4、小结
希望通过本文你能够了解到并发编程带来的复杂性和相关问题。并发编程中,看起来,无论是多么简单的API,由此产生的问题会变得非常的难以观测,并且要想调试这类问题,往往都是比较困难的。
 
另外,并发实际上是一个非常棒的功能——它充分利用了现代多核CPU的强大计算能力。在开发中,关键的一点就是尽量让并发模型简单,这样可以限制锁的数量。
 
我们建议采纳的安全模式是这样的:从主线程中提取出使用到的数据,并利用一个操作队列在后台处理相关的数据,然后将后台处理的结果反馈到主队列中。使用这种方式,开发者不需要自己负责任何的锁,这也就减少了犯错误的概率。
 
 
posted @ 2015-05-06 11:37  sunminmin2011  阅读(356)  评论(0编辑  收藏  举报