术语
进程,线程,任务
进程(process),指的是一个正在运行中的可执行文件。每一个进程都拥有独立的虚拟内存空间和系统资源,包括端口权限等,且至少包含一个主线程和任意数量的辅助线程。另外,当一个进程的主线程退出时,这个进程就结束了;
线程(thread),指的是一个独立的代码执行路径,也就是说线程是代码执行路径的最小分支。在 iOS 中,线程的底层实现是基于 POSIX threads API 的,也就是我们常说的 pthreads ;
任务(task),指的是我们需要执行的工作,是一个抽象的概念,用通俗的话说,就是一段代码。
串行,并发
串行和并发的主要区别在于允许同时执行的任务数量。
串行,指的是一次只能执行一个任务,必须等一个任务执行完成后才能执行下一个任务;
并发,则指的是允许多个任务同时执行。
从本质上来说,串行和并发的主要区别在于允许同时执行的任务数量。串行,指的是一次只能执行一个任务,必须等一个任务执行完成后才能执行下一个任务;并发,则指的是允许多个任务同时执行。 不管是串行队列还是并发队列都是一个一个的取出任务,区别在于串行队列只能有一个任务正在执行,也就是说取出一个任务执行后,要等这个任务执行完成后,才能取下一个任务;而并发队列就没有这个限制,它取出一个任务执行后,不需要管这个任务有没有执行完成,接着取下一个任务来执行,再取下下一个,而任务的个数限制取决于系统的负载等情况。
同步,异步
同步和异步操作的主要区别在于是否等待操作执行完成,亦即是否阻塞当前线程。
同步操作会等待操作执行完成后再继续执行接下来的代码,
而异步操作则恰好相反,它会在调用后立即返回,不会等待操作的执行结果。
队列,线程
在 iOS 中,有两种不同类型的队列,分别是串行队列和并发队列。
串行队列一次只能执行一个任务,而并发队列则可以允许多个任务同时执行。
iOS 系统就是使用这些队列来进行任务调度的,它会根据调度任务的需要和系统当前的负载情况动态地创建和销毁线程,而不需要我们手动地管理。
-> 尽量使用队列,少用线程
使用线程的场景
1.用线程以外的其他任何方式都不能实现我们的特定任务;
2.必须实时执行一个任务。因为虽然队列会尽可能快地执行我们提交的任务,但是并不能保证实时性;
3.你需要对在后台执行的任务有更多的可预测行为。
Operation Queue 和 GCD
Operation Queues :我们可以给 operation 之间添加依赖关系、取消一个正在执行的 operation 、暂停和恢复 operation queue 等. 增加一点点额外的开销
GCD: 则是一种更轻量级的,以 FIFO 的顺序执行并发任务的方式,使用 GCD 时我们并不关心任务的调度情况,而让系统帮我们自动处理。但是 GCD 的短板也是非常明显的,比如我们想要给任务之间添加依赖关系、取消或者暂停一个正在执行的任务时就会变得非常棘手。
并发 vs. 非并发 Operation
通常来说,我们都是通过将 operation 添加到一个 operation queue 的方式来执行 operation 的,然而这并不是必须的。我们也可以直接通过调用 start 方法来执行一个 operation ,但是这种方式并不能保证 operation 是异步执行的。NSOperation 类的 isConcurrent 方法的返回值标识了一个 operation 相对于调用它的 start 方法的线程来说是否是异步执行的。在默认情况下,isConcurrent 方法的返回值是 NO ,也就是说会阻塞调用它的 start 方法的线程。
如果我们想要自定义一个并发执行的 operation ,那么我们就必须要编写一些额外的代码来让这个 operation 异步执行。比如,为这个 operation 创建新的线程、调用系统的异步方法或者其他任何方式来确保 start 方法在开始执行任务后立即返回。
在绝大多数情况下,我们都不需要去实现一个并发的 operation 。如果我们一直是通过将 operation 添加到 operation queue 的方式来执行 operation 的话,我们就完全没有必要去实现一个并发的 operation 。因为,当我们将一个非并发的 operation 添加到 operation queue 后,operation queue 会自动为这个 operation 创建一个线程。因此,只有当我们需要手动地执行一个 operation ,又想让它异步执行时,我们才有必要去实现一个并发的 operation 。
NSBlockOperation和GCD
当我们在应用中已经使用了 Operation Queues 且不想创建 Dispatch Queues 时,NSBlockOperation 类可以为我们的应用提供一个面向对象的封装;
我们需要用到 Dispatch Queues 不具备的功能时,比如需要设置 operation 之间的依赖关系、使用 KVO 观察 operation 的状态变化等。
自定义 Operation 对象
非并发Operation
可以自定义非并发和并发两种不同类型的 NSOperation 子类。
至少实现以下两个方法:
一个自定义的初始化方法;
main 方法。
- (id)initWithData:(id)data {
self = [super init];
if (self) {
self.data = data;
}
return self;
}
/// 不支持取消操作
- (void)main {
@try {
NSLog(@"Start executing %@ with data: %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), self.data, [NSThread mainThread], [NSThread currentThread]);
sleep(3);
NSLog(@"Finish executing %@", NSStringFromSelector(_cmd));
}
@catch(NSException *exception) {
NSLog(@"Exception: %@", exception);
}
}
在以下几个关键点检查 isCancelled 方法的返回值:
在真正开始执行任务之前;
至少在每次循环中检查一次,而如果一次循环的时间本身就比较长的话,则需要检查得更加频繁;
在任何相对来说比较容易中止 operation 的地方。
/// 支持取消操作
- (void)main {
@try {
if (self.isCancelled) return;
NSLog(@"Start executing %@ with data: %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), self.data, [NSThread mainThread], [NSThread currentThread]);
for (NSUInteger i = 0; i < 3; i++) {
if (self.isCancelled) return;
sleep(1);
NSLog(@"Loop %@", @(i + 1));
}
NSLog(@"Finish executing %@", NSStringFromSelector(_cmd));
}
@catch(NSException *exception) {
NSLog(@"Exception: %@", exception);
}
}
并发执行的 Operation
如果你想要手动地执行一个 operation ,又想这个 operation 能够异步执行的话,你需要做一些额外的配置来让你的 operation 支持并发执行。下面列举了一些你可能需要重写的方法:
start :必须的,所有并发执行的 operation 都必须要重写这个方法,替换掉 NSOperation 类中的默认实现。start 方法是一个 operation 的起点,我们可以在这里配置任务执行的线程或者一些其它的执行环境。另外,需要特别注意的是,在我们重写的 start 方法中一定不要调用父类的实现;
main :可选的,通常这个方法就是专门用来实现与该 operation 相关联的任务的。尽管我们可以直接在 start 方法中执行我们的任务,但是用 main 方法来实现我们的任务可以使设置代码和任务代码得到分离,从而使 operation 的结构更清晰;
isExecuting 和 isFinished :必须的,并发执行的 operation 需要负责配置它们的执行环境,并且向外界客户报告执行环境的状态。因此,一个并发执行的 operation 必须要维护一些状态信息,用来记录它的任务是否正在执行,是否已经完成执行等。此外,当这两个方法所代表的值发生变化时,我们需要生成相应的 KVO 通知,以便外界能够观察到这些状态的变化;
isConcurrent :必须的,这个方法的返回值用来标识一个 operation 是否是并发的 operation ,我们需要重写这个方法并返回 YES 。
@implementation OQConcurrentOperation
@synthesize executing = _executing;
@synthesize finished = _finished;
- (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;
}
@end
executing 和 finished 属性都被声明成了只读的 readonly 。所以我们在 NSOperation 子类中就没有办法直接通过 setter 方法来自动触发 KVO 通知,这也是为什么我们需要在接下来的代码中手动触发 KVO 通知的原因。
- (void)start {
if (self.isCancelled) {
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
_executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main {
@try {
NSLog(@"Start executing %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), [NSThread mainThread], [NSThread currentThread]);
sleep(3);
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
[self didChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
NSLog(@"Finish executing %@", NSStringFromSelector(_cmd));
}
@catch (NSException *exception) {
NSLog(@"Exception: %@", exception);
}
}
注意,有一个非常重要的点需要引起我们的注意,那就是即使一个 operation 是被 cancel 掉了,我们仍然需要手动触发 isFinished 的 KVO 通知。因为当一个 operation 依赖其他 operation 时,它会观察所有其他 operation 的 isFinished 的值的变化,只有当它依赖的所有 operation 的 isFinished 的值为 YES 时,这个 operation 才能够开始执行。因此,如果一个我们自定义的 operation 被取消了但却没有手动触发 isFinished 的 KVO 通知的话,那么所有依赖它的 operation 都不会执行。
依赖,优先级
不要在 operation 之间添加循环依赖,因为这样会导致这些 operation 都不会被执行。
注意,我们应该在手动执行一个 operation 或将它添加到 operation queue 前配置好依赖关系,因为在之后添加的依赖关系可能会失效。
注意,我们只能够在执行一个 operation 或将其添加到 operation queue 前,通过 operation 的 setThreadPriority: 方法来修改它的线程优先级。当 operation 开始执行时,NSOperation 类中默认的 start 方法会使用我们指定的值来修改当前线程的优先级。另外,我们指定的这个线程优先级只会影响 main 方法执行时所在线程的优先级。所有其它的代码,包括 operation 的 completion block 所在的线程会一直以默认的线程优先级执行。因此,当我们自定义一个并发的 operation 类时,我们也需要在 start 方法中根据指定的值自行修改线程的优先级。
Completion Block
从 iOS 4.0 开始,一个 operation 可以在它的主任务执行完成时回调一个 completion block 。我们可以用 completion block 来执行一些主任务之外的工作,比如,我们可以用它来通知一些客户 operation 已经执行完毕,而并发的 operation 也可以用这个 block 来生成最终的 KVO 通知。如果需要设置一个 operation 的 completion block ,直接调用 NSOperation 类的 setCompletionBlock: 方法即可。
注意,当一个 operation 被取消时,它的 completion block 仍然会执行,所以我们需要在真正执行代码前检查一下 isCancelled 方法的返回值。另外,我们也没有办法保证 completion block 被回调时一定是在主线程,理论上它应该是与触发 isFinished 的 KVO 通知所在的线程一致的,所以如果有必要的话我们可以在 completion block 中使用 GCD 来保证从主线程更新 UI 。
Operation Queue
可以通过 setMaxConcurrentoperationCount: 方法来设置一个 operation queue 最大可并发的 operation 数,因此将这个值设置成 1 就可以实现让 operation queue 一次只执行一个 operation 的目的。但是需要注意的是,虽然这样可以让 operation queue 一次只执行一个 operation ,但是 operation 的执行顺序还是一样会受其他因素影响的,比如 operation 的 isReady 状态、operation 的队列优先级等。因此,一个串行的 operation queue 与一个串行的 dispatch queue 还是有本质区别的,因为 dispatch queue 的执行顺序一直是 FIFO 的。如果 operation 的执行顺序对我们来说非常重要,那么我们就应该在将 operation 添加到 operation queue 之前就建立好它的依赖关系。
唯一从 operation queue 中出队一个 operation 的方式就是调用它的 cancel 方法取消这个 operation ,或者直接调用 operation queue 的 cancelAllOperations 方法取消这个 operation queue 中所有的 operation 。
注意,我们应该要坚决避免在主线程中去同步等待一个 operation 的执行结果,阻塞的方式只应该用在辅助线程或其他 operation 中。因为阻塞主线程会大大地降低我们应用的响应性,带来非常差的用户体验。
如果我们想要暂停和恢复执行 operation queue 中的 operation ,可以通过调用 operation queue 的 setSuspended: 方法来实现这个目的。不过需要注意的是,暂停执行 operation queue 并不能使正在执行的 operation 暂停执行,而只是简单地暂停调度新的 operation 。另外,我们并不能单独地暂停执行一个 operation ,除非直接 cancel 掉。
网友评论