首页 > 编程语言 > 详细

多线程理解

时间:2014-06-05 13:28:20      阅读:567      评论:0      收藏:0      [点我收藏+]

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在系统中处于不同的地方。并发编程对于开发者来说非常的强大,但是作为开发者需要担负很大的责任,来把任务处理好。

实际上,并发编程是一个很有挑战的主题,它有许多错综复杂的问题和陷阱,当开发者在使用类似GCD或NSOperationQueue 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。

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 < info-&gt;count; ++i) {

uint32_t v = info-&gt;inputValues[i];

min = MIN(min, v);

max = MAX(max, v);

}

free(arg);

struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));

result-&gt;min = min;

result-&gt;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 < threadCount; ++i) { struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info)); size_t offset = (count / threadCount) * i; info-&gt;inputValues = inputValues + offset;

info-&gt;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 < threadCount; ++i) { min = MIN(min, results[i]-&gt;min);

max = MAX(max, results[i]-&gt;max);

free(results[i]);

results[i] = NULL;

}

NSLog(@"min = %u", min);

NSLog(@"max = %u", max);

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都将被放入到系统的线程池的一个全局队列中。

bubuko.com,布布扣

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

虽然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,都会立即退出。

关于并发编程中面临的挑战,会在下一篇文章中出现。

 

多线程理解,布布扣,bubuko.com

多线程理解

原文:http://www.cnblogs.com/android-dev/p/3769564.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!