简介
NSOperation、NSOperationQueue 是苹果提供的一套多线程解决方案,是基于GCD更高一层的封装,完全面向对象。但比 GCD 更简单易用、代码可读性更高。
- 好处: 可添加完成的代码块
complete,在操作完成后执行。 添加操作之间的依赖关系,方便控制执行顺序。 设定操作执行的优先级。 可以取消一个操作的执行。 使用 KVO 观察执行状态:isExecuteing、isFinished、isCancelled。
在 NSOperation、NSOperationQueue 中也有类似的“任务”和“队列”的概念。
1.NSOperation
任务,或操作。即,你在线程中执行的那段代码。NSOperation是一个抽象类,要使用其子类 NSInvocationOperation、NSBlockOperation,或者自定义子类来封装任务。
2.NSOperationQueue
指用来存放操作的队列。
-
队列类型 主队列、自定义队列。 主队列运行在主线程之上,而自定义队列在后台执行。
-
任务状态和顺序 不同于 GCD 中的队列 FIFO(先进先出)的原则。对于添加到
NSOperationQueue中的任务,首先进入准备就绪的状态(就绪状态取决于任务之间的依赖关系),然后进入就绪状态的任务的开始执行顺序(而非执行完成的顺序)由任务之间相对的优先级决定。 -
并发和串行 任务队列通过设置最大并发操作数
maxConcurrentOperationCount来控制并发、串行。
3. NSOperation实现多线程
默认情况下,NSOperation 单独使用时,系统同步执行操作。只有配合 NSOperationQueue 才能更好地实现异步执行。
- 使用步骤分为3步
创建任务:先将需要执行的任务,封装到一个
NSOperation对象中。 创建队列:即NSOperationQueue对象。 将任务加入到队列中:将NSOperation对象添加到NSOperationQueue对象中。
系统就会自动将NSOperationQueue中的 NSOperation 取出来,在新线程中执行操作。
3.1创建NSOperation任务
NSOperation 是个抽象类,不能直接用来封装任务。要使用它的子类来封装任务:NSInvocationOperation、NSBlockOperation
以及自定义的子类。
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
[op start]; // 开始执行
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
// do some thing...
}];
[op start];
此外,可以为任务对象添加额外任务。
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
// do something...
}];
// 添加额外的操作
[op addExecutionBlock:^{
// do otherthing...
}];
- 在没有使用
NSOperationQueue、在主线程中单独使用NSOperation的子类对象 执行一个任务的情况下,任务是在当前线程执行的,并没有开启新线程。- 使用子类
NSBlockOperation,并调用方法addExecutionBlock:的情况下,如果添加的操作的个数多,就会自动开启新线程。
2.创建NSOperationQueue队列
有两种:主队列、自定义队列。
- 主队列
NSOperationQueue *queue = [NSOperationQueue mainQueue];
凡是添加到主队列中的操作,都会放到主线程中执行。
- 自定义队列 添加到这种队列中的操作,就会自动放到子线程中执行;同时包含串行、并发功能。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue. maxConcurrentOperationCount = 3;
可设置属性maxConcurrentOperationCount最大并发操作数,决定串行、并发。
属性
maxConcurrentOperationCount= 1; // 串行队列,任务只能按顺序串行执行的 = 2; // 并发队列,>2时,任务是并发执行的,可同时执行多个操作 = 8; // 并发队列 当然这个值不应超过系统限制,即使设置了一个很大的值,系统也会自动调整为 min。
3.将操作加入到队列中
方法addOperation:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
//.....
}];
[op3 addExecutionBlock:^{
//.....
}];
[queue addOperation:op1];
[queue addOperation:op3];
方法addOperationWithBlock:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 添加操作到队列中
[queue addOperationWithBlock:^{
//.....
}];
操作和操作队列、使用步骤和基本使用方法、控制串行/并发执行、
- 把任务添加到队列中,相当于调用了任务的start方法
NSOperation 依赖
通过依赖,可以很方便的控制任务之间的执行先后顺序。
addDependency:(NSOperation *)op;添加依赖removeDependency:(NSOperation *)op;移除依赖NSArray<NSOperation *> *dependencies;在当前任务开始执行之前,已完成任务对象的数组
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
//.....
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
//.....
}];
// 添加依赖
[op2 addDependency:op1]; //让op2 依赖于 op1。表示先执行op1
[queue addOperation:op1];
[queue addOperation:op2];
- 准备就绪状态 当一个任务的所有依赖都已经完成时,任务对象通常会进入准备就绪状态,等待执行。
###NSOperation 优先级
NSOperation 提供了优先级属性queuePriority 。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但可以通过setQueuePriority:方法来改变在同一队列中的任务的优先级。
queuePriority属性适用于同一队列中的任务,不同队列的任务不适用。 优先级的取值:NSOperationQueuePriorityVeryLow = -8L,NSOperationQueuePriorityLow = -4L,NSOperationQueuePriorityNormal = 0,(默认)NSOperationQueuePriorityHigh = 4,NSOperationQueuePriorityVeryHigh = 8
优先级与依赖
- 优先级属性
queuePriority决定了进入准备就绪状态下的任务之间的开始执行顺序。并且,优先级不能取代依赖关系。 - 如果一个队列中既包含高优先级任务,又包含低优先级任务,并且两个任务都已经准备就绪,那么队列先执行高优先级任务。
- 优先级不能取代依赖关系。 如果,一个队列中既包含了准备就绪状态的任务A,又包含了未准备就绪的任务B,B的优先级比A高。那么,会优先执行B。如果要控制操作间的启动顺序,则必须使用依赖关系。
线程间的通信
处理完子行程的任务,返回主行程处理
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperationWithBlock:^{
// .....
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 回到主线程, 进行一些 UI 刷新等操作
}];
}];
取消线程
正在执行的操作,无法取消。只能取消未执行的操作。
1.可通过 cancel 方法,取消未执行的单个操作。
NSOperationQueue *queue1 = [[NSOperationQueue alloc]init];
NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block11");
}];
NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block22");
}];
NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block33");
}];
[block3 cancel];
[queue1 addOperations:@[block1,block2,block3] waitUntilFinished:YES];
- cancelAllOperations方法,但无法移除 正在执行的操作。
// 移除队列里面所有的操作
[queue1 cancelAllOperations];
3.挂起队列,使队列任务不再执行,但正在执行的操作无法挂起。
queue1.suspended = YES;
要实现取消正在执行的操作,可以自定义NSOperation,其实就是拦截main方法。 main方法: 1.任何操作在执行时,首先会调用start方法,它会更新操作的状态过滤操作(如过滤掉处于“取消”状态的操作) 2.经start方法过滤后,只有正常可执行的操作,就会调用main方法。 3.重写操作的入口方法(main),就可以在这个方法里面指定操作执行的任务。 4.main方法默认是在子线程异步执行的。
线程同步和线程安全
-
线程安全 有多个线程可能会同时运行一段代码。如果每次运行结果和单线程运行的结果一样,且其他变量的值也和预期的是一样的,就是线程安全的。 若每个线程中对全局变量、静态变量只有读操作(无写操作),一般这个变量是线程安全的。 若有多个线程同时执行写操作(更改变量的值),就需要考虑线程同步,否则就可能影响线程安全。
-
线程同步 可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。 例子:火车票售卖
线程安全解决方案:加锁
可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。
iOS 实现线程加锁有很多种方式:
dispatch_semaphore:建议使用,性能较好NSLock@synchronized:性能最差os_unfair_lock:iOS10开始NSConditionNSConditionLockpthread_muteOSSpinLock:iOS10 废弃atomic(property) set/get:原子操作
思想:执行写操作前,上锁,写操作完,解锁。
自旋锁: 会忙等。所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。 适用的情况 预计线程等待锁的时间很短 加锁的代码(临界区)经常被调用,但竞争情况很少发生 CPU资源不紧张、多核处理器
互斥锁: 会休眠。 所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁。此时会唤醒休眠线程。 适用的情况 预计线程等待锁的时间较长 临界区竞争非常激烈 临界区有IO操作 临界区代码复杂或者循环量大 单核处理器
死锁: 通常是指2个操作相互等待对方完成,造成死循环,于是2个操作都无法进行,就产生了死锁。
常用属性和方法归纳
.
NSOperation 和GCD的对比
-
NSOperation 是对线程的高度抽象,在项目中使 用它,会使项目的程序结构更好。子类化 NSOperation 的设计思路,是具有面向对 象的优点(复用、封装),使得实现是多线程支持,而接口简单。 建议在复杂项目中 使用。
-
GCD 本身非常简单、易用,对于不复杂的多线程操 作,会节省代码量,而 Block 参数的使用,会是代码更为易读。 建议在简单项目中 使用。
-
NSOperation的优点
1.NSOperationQueue可以轻松在Operation间设置依赖关系,而GCD 需要写很多代码才能实现。
2. NSOperation可以很方便地调整执⾏顺序、设置最⼤并发数。
3.NSOperationQueue⽀支持KVO,可以监测operation的状态:是否正在执行 (isExecuted)、 是否结束(isFinished),是否取消(isCanceld)
4.GCD只⽀支持FIFO的队列列