GCD vs NSOperation

GCD 和 NSOperation都是iOS多线程技术,在现在的实现中NSOperation还是基于GCD的封装。GCD是C-based API,把一个任务单元封装在一个block中,可以在c、c++中使用;NSOperation是Cocoa提供的,是一个objective-c抽象类,使用时你要继承它或直接使用系统提供的NSInvocationOperationNSBlockOperation类。
接下来我会分别对GCD和NSOperation进行介绍:

GCD

Dispatch Queues

说到GCD肯定要说到dispatch queues,因为你每次使用GCD提交任务时必须指定一个queue。dispatch queues可以说是GCD的核心,它控制着你提交的任务是串行还是并行。

首先,dispatch queue是一个队列,队列的特点就是FIFO(First In, First Out)。这意味着你按怎样顺序提交任务,它就按什么顺序开始这些任务。

其次,dispatch queue主要分为两大种类:Serial(串行)Concurrent(并发)Serial意味着队列中一次只能运行一个任务,必须等这个任务完了,才能开始下一任务;Concurrent意味着可以并发运行队列中多个任务,不必等一个任务完就可以开始下个任务。不过这两个都有一个共同点,就是任务开始的顺序还是要跟加入顺序一样。

最后,介绍下创建或获取这两种queue的方法。
1.获取或创建Serial queue:

1
2
3
4
5
6
//自己创建一个Serial queue
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);

//获取main queue,main queue就是一个serial queue,不过里面task运行在主线程,使用时要注意
dispatch_queue_t mainQueue = dispatch_get_main_queue()

2.获取或创建Concurrent queue,系统默认按不同优先级为我们创建了4个Concurrent queues。iOS 5后,也可以自己指定DISPATCH_QUEUE_CONCURRENT创建自己的concurrent queue

1
2
3
4
5
//获取系统的concurrent queue
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//创建自己的concurrent queue
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.MyQueue", DISPATCH_QUEUE_CONCURRENT);

默认的四个concurrent queue优先级

1
2
3
4
DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND

并发遍历一个数组

我们可以使用dispatch_apply并发遍历一个数组,这在一些场景还是非常有用的

1
2
3
4
5
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(count, queue, ^(size_t i) {
printf("%u\n",i);
});

dispatch_apply会生成count个block,加入queue队列,并发执行,当所有block都执行完,dispatch_apply才会返回,所以dispatch_apply也是个同步操作。

任务同步(依赖)

比如有三个任务A、B、C,C任务得等A、B都执行完才能开始,而A、B之间倒没这种要求,这样C就是依赖于A、B,或我们先并发执行A、B,然后得在一个地方,同步A、B任务(等它们都执行完),再开始C任务。这时用dispatch_group_tdispatch_group_async dispatch_group_notify可以满足我们需求

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
// 开始任务A
});
dispatch_group_async(group, queue, ^{
// 开始任务B
});
//使用 dispatch_group_notify 会自动帮我们release group,如果使用dispatch_group_wait,我们要手动release group:dispatch_release(group)
dispatch_group_notify(group, queue, ^{
// 这时A、B任务都执行完了,我们可以开始C任务
});

使用 Dispatch Semaphores 限制资源访问

系统的一些资源是有限的,比如I/O资源,这时可以使用dispatch semaphores限制最大访问数量

1
2
3
4
5
6
7
8
9
10
11
12
// Create the semaphore, specifying the initial pool size
dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);

// Wait for a free file descriptor
// 会将fd_sema数量减1,如果小于0,就阻塞线程,直到fd_sema大于0
dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
fd = open("/etc/services", O_RDONLY);

// Release the file descriptor when done
close(fd);
//将fd_sema加1,让其它被阻塞线程继续工作
dispatch_semaphore_signal(fd_sema);

同步共享资源

我们有时会有希望同一个时间只有一个线程访问同一共享资源需求,这时可以使用Serail queue。当然,在创建dispatch semaphore时传入1也可以达到该目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dispatch_queue_t queue = dispatch_queue_create("com.example.MyQueue", NULL);

- (id)someThing {
__block id thisSomeThing;
dispatch_sync(queue, ^{
thisSomeThing = _someThing;
})
return thisSomeThing;
}

- (void)setSomeThing:(id)newSomeThing {
dispatch_async(queue, ^{
if(newSomeThing != _someThing) {
_someThing = newSomeThing;
//do some other work
}
})
}

NSOperation

状态

一个NSOperation有多种状态,并且支持KVO

1
2
3
4
isCancelled
isExecuting
isFinished
isReady

ready —> executing —> finished 是常见的状态路径

使用NSOperation

有三种方法可以把任务封装到NSOperation方法

1.通过NSInvocationOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
@implementation MyCustomClass
- (NSOperation*)taskWithData:(id)data {
NSInvocationOperation* theOp = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(myTaskMethod:) object:data];

return theOp;
}

// This is the method that does the actual work of the task.
- (void)myTaskMethod:(id)data {
// Perform the task.
}
@end

2.通过NSBlockOperation

1
2
3
4
5
6
7
NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
NSLog(@"Beginning operation.\n");
// Do some work.
}];
[theOp addExecutionBlock:^{
//add another block
}];

NSBlockOperation可以添加多个block,一般来说这些block是并发执行的,只有这些block都执行完,这个operation才算finished。如果想让这些block串行执行,可以创建一个NSOperationQueue,然后为underlyingQueue赋值一个串行的dispatch_queue_t(不能是dispatch_get_main_queue),最后把该operation添加到该NSOperationQueue。看起来很麻烦吧,所以不推荐这种做法来做串行。

3.自定义NSOperation

需要继承NSOperation,至少实现两个函数:自定义初始化函数,main函数。NSOperation可以分为两个种类:异步和非异步。异步就是还起了个线程处理任务;非异步不创建新线程。下面是一个自定义异步operation的例子。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@interface MyOperation : NSOperation {
BOOL executing;
BOOL finished;
}
- (void)completeOperation;
@end

@implementation MyOperation
- (id)init {
self = [super init];
if (self) {
executing = NO;
finished = NO;
}
return self;
}

- (BOOL)isConcurrent {
return YES;
}

- (BOOL)isExecuting {
return executing;
}

- (BOOL)isFinished {
return finished;
}

- (void)start {
// Always check for cancellation before launching the task.
if ([self isCancelled])
{
// Must move the operation to the finished state if it is canceled.
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}

// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}

- (void)main {
@try {

// Do the main work of the operation here.

[self completeOperation];
}
@catch(...) {
// Do not rethrow exceptions.
}
}

- (void)completeOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];

executing = NO;
finished = YES;

[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
@end

使用start启动一个operation

要注意观察isReady是否为YES,如果是NO,说明该operation的依赖还没执行完。默认start会在当前调用start的线程处理任务。

1
2
3
4
5
6
7
- (BOOL)performOperation:(NSOperation*)anOp
{
if ([anOp isReady] && ![anOp isCancelled])
{
[anOp start];
}
}

Operation Queues

我们可以通过将一个operation添加到一个NSOperationQueue,让NSOperationQueue自动管理operation。NSOperationQueue会检查operation isReady,如果 isReady==YES,则在适当时候分配线程start这个operation。所以用NSOperationQueue来启动线程还是有很多好处,推荐用这种方法。

1
2
3
4
5
6
NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];
[aQueue addOperation:anOp]; // Add a single operation
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations
[aQueue addOperationWithBlock:^{
/* Do something. */
}];

注意:NSOperationQueue并不是按FIFO启动operation,它会考虑多种因素:优先级,isReady,提交顺序。所以不要用NSOperation来做串行,可以试着用依赖链,下面会介绍。

NSOperationQueue还支持最大并发数量:maxConcurrentOperationCount

取消operation

可以直接调用 cancel 函数。operation内部会做一些清理工作,最后把 cancelled和finished置为YES。如果在NSOperationQueue中,queue会自动把finished==YES的operation移除。

优先级(NSOperationQueuePriority)

我们可以为每个operation添加优先级。优先级越高,意味着有机会越快启动。

1
2
3
4
5
6
7
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};

服务质量(Quality of Service)

服务质量等级越高,意味着你可以你可以获得更多的资源(cpu,内存、网络、磁盘),从而有更快的执行速度。

1
2
3
4
5
6
7
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
} NS_ENUM_AVAILABLE(10_10, 8_0);

上面等级重上往下降低,不过NSQualityOfServiceDefault不确定,可能介于UserInitiated和Utility之间。

1
2
3
4
5
NSOperation *backgroundOperation = [[NSOperation alloc] init];
backgroundOperation.queuePriority = NSOperationQueuePriorityLow;
backgroundOperation.qualityOfService = NSOperationQualityOfServiceBackground;

[[NSOperationQueue mainQueue] addOperation:backgroundOperation];

任务依赖

NSOperation可以比GCD更方便使用依赖

1
2
3
4
5
6
7
NSOperation *networkingOperation = ...
NSOperation *resizingOperation = ...
[resizingOperation addDependency:networkingOperation];

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperation:networkingOperation];
[operationQueue addOperation:resizingOperation];

resizingOperation只有在networkOperation执行完后才会启动。

completionBlock

有时任务完成后,可能要执行一些回调,这时completionBlock就可以派上用场

1
2
3
4
5
6
7
NSOperation *operation = ...;
operation.completionBlock = ^{
NSLog("Completed");
};

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperation:operation];

GCD 和 NSOperation 怎么选择

这个问题其实怎么方便怎么来,下面是网上一些人经验。

优先考虑NSOperation情况:

  • 任务之间有依赖关系
  • 限制最大可执行任务数量
  • 任务可能被取消
  • 需要执行回调

优先考虑GCD情况:

  • 任务只是简单block提交
  • 任务之间需要复杂block嵌套
  • 任务需要非常频繁提交(因为NSOperation是对象,频繁创建也很耗
    CPU和内存)

ps:
Concurrency Programming Guide
NSOperation
iOS 多线程开发之OperationQueue(二)NSOperation VS GCD