iOS中的并发编程

并发编程,这个我们听起来再熟悉不过了,我们在代码编写过程中也经常会做这样一些操作,但是我发现,在实际的编写过程中,很多实用并发编程的方式,其实是错误使用的,当然这也包括我自己,对一些并发编程也是糊里糊涂,今天的工作中涉及到了许多的并发方面的问题,专门抽时间去学习了一下,现在记录下来。

OS X和iOS中的并发编程

在OS X和iOS中,都为我们提供了相同的并发编程API。pthread 、 NSThread 、GCD 、NSOperationQueue。

pthread

pthread是比较底层的并发API,这个用起来并不是那么容易,而且在我们日常的编码过程中,我们也应该抛弃掉这种效率极低的并发编程方式。这里就不对这种并发做过多详细的介绍。

NSThread

NSThread这个我们听起来就很熟悉了,实际上,这是对pthread的一个封装,封装成了Objectivc-C的接口API,在cocoa环境中,我们能够轻易使用NSThread来进行并发编程。

例如我们现在有这样一个场景,需要计算100万个数字中的最大数和最小数,首先我们可以定义一个NSThread的子类,专门来进行这个运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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;
// 进行相关数据的处理
self.min = min;
self.max = max;
}
@end

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

1
2
3
4
5
6
7
8
9
10
11
12
NSMutableSet *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];
}

虽然,看起来这个多线程实现起来比较简单,但是呢,在实际的编码中,我们并不会采用这种方式来进行,并发编程。

这种方式,其实是我们自己来操作线程,进行并发编程,这样就涉及到一个问题,例如,我们在使用到AFNetworking 进行网络访问的时候,本身AF中已经对网络访问进行了异步的线程处理,如果我们在调用AF的时候,再次进行并发线程处理,那么我们使用NSThread这种方式进行并发编程的时候,就回造成线程的指数级增长,因为我们操作的都是单个的线程。开了这么多的线程,当然会造成内存和CPU的高度浪费,而且会造成其他的一些不必要的bug。

那正确的并发编程 的姿势 是什么,当然是 GCD 和 operation queue ——基于队列的并发编程。这两种方式,通过管理一个被大家协同使用的线程池,来解决上面的问题。

Grand Central Dispatch(GCD)

通过 GCD,开发者不用再直接跟线程打交道了,只需要向队列中添加代码块即可,GCD 在后端管理着一个线程池。GCD 不仅决定着你的代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理。这样可以将开发者从线程管理的工作中解放出来,通过集中的管理线程,来缓解大量线程被创建的问题。

GCD 带来的另一个重要改变是,作为开发者可以将工作考虑为一个队列,而不是一堆线程,这种并行的抽象模型更容易掌握和使用。

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

使用不同优先级的若干个队列乍听起来非常直接,不过,我们强烈建议,在绝大多数情况下使用默认的优先级队列就可以了。如果执行的任务需要访问一些共享的资源,那么在不同优先级的队列中调度这些任务很快就会造成不可预期的行为。这样可能会引起程序的完全挂起,因为低优先级的任务阻塞了高优先级任务,使它不能被执行。

稍后我们将详细介绍GCD的使用

Operation Queues

操作队列是在GCD之上,实现了一些更方便的功能,更高级的AP,这些功能对于开发者来讲通常来说,是最安全的最好的选择。

NSOperationQueue 有两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。在两种类型中,这些队列所处理的任务都使用 NSOperation 的子类来表述。

你可以通过重写 main 或者 start 方法 来定义自己的 operations 。前一种方法非常简单,开发者不需要管理一些状态属性(例如 isExecuting 和 isFinished),当 main 方法返回的时候,这个 operation 就结束了。这种方式使用起来非常简单,但是灵活性相对重写 start 来说要少一些。

1
2
3
4
5
6
@implementation YourOperation
- (void)main
{
// 进行处理 ...
}
@end

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@implementation YourOperation
- (void)start
{
self.isExecuting = YES;
self.isFinished = NO;
// 开始处理,在结束时应该调用 finished ...
}

- (void)finished
{
self.isExecuting = NO;
self.isFinished = YES;
}
@end

注意:这种情况下,你必须手动管理操作的状态。 为了让操作队列能够捕获到操作的改变,需要将状态的属性以配合 KVO 的方式进行实现。如果你不使用它们默认的 setter 来进行设置的话,你就需要在合适的时候发送合适的 KVO 消息。

为了能使用操作队列所提供的取消功能,你需要在长时间操作中时不时地检查 isCancelled 属性:

1
2
3
4
5
6
- (void)main
{
while (notDone && !self.isCancelled) {
// 进行处理
}
}

当你定义好 operation 类之后,就可以很容易的将一个 operation 添加到队列中:

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

另外,你也可以将 block 添加到操作队列中。这有时候会非常的方便,比如你希望在主队列中调度一个一次性任务:

1
2
3
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 代码...
}];

虽然通过这种的方式在队列中添加操作会非常方便,但是定义你自己的 NSOperation 子类会在调试时很有帮助。如果你重写 operation 的description 方法,就可以很容易的标示出在某个队列中当前被调度的所有操作 。

除了提供基本的调度操作或 block 外,操作队列还提供了在 GCD 中不太容易处理好的特性的功能。例如,你可以通过 maxConcurrentOperationCount 属性来控制一个特定队列中可以有多少个操作参与并发执行。将其设置为 1 的话,你将得到一个串行队列,这在以隔离为目的的时候会很有用。

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

1
2
3
[intermediateOperation addDependency:operation1];
[intermediateOperation addDependency:operation2];
[finishedOperation addDependency:intermediateOperation];

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

从本质上来看,操作队列的性能比 GCD 要低那么一点,不过,大多数情况下这点负面影响可以忽略不计,操作队列是并发编程的首选工具。

参考:

https://www.objccn.io/issue-2-1/