文章目录
简介概念了解
在SDWebImage中有见过NSOperation这个函数
关于NSOperation我们可以看到官方文档中的解释:
表示于单个任务相关联的代码和数据的抽象类
因为其是一个抽象类,我们不能直接使用它,而是使用它的子类或使用系统定义的子类(NSInvocationOperation或NSBlockOperation)来执行实际的任务。尽管是抽象的,NSOperation的基本实现确实包含了重要的逻辑来协调任务的安全执行。这种内置逻辑的存在允许您将注意力集中在任务的实际实现上,而不是确保它与其他系统对象正确公所所需的粘合代码上。
通常通过将操作添加到操作队列(NSOperationQueue类的实例)来执行操作。
如果不想使用操作队列,可以直接从代码中调用操作的start方法来执行操作。手动执行操作会给代码带来更大的负担,因为启动未处于就绪状态的操作会触发异常。
操作队列是OC中的一种高级的并发处理方法,现在它是基于GCD更高一层的封装,完全面向对象。但是比GCD更加简单易用、代码可读性也更高。
操作队列不同于GCD中FIFO的原则。NSOperationQueue对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作开始执行顺序由操作之间的优先级决定。
操作队列也为我们提供了两种不同类型的队列:主队列和自定队列,主队列运行在主线程之上,而自定义队列在后台执行
为什么我们需要使用NSOperation、NSOperationQueue
- 可以添加完成的代码块,在操作完成之后执行
- 添加操作之间的依赖关系,方便的控制执行顺序
- 设定操作执行的优先级
- 可以很方便的取消一个操作的执行
- 使用KVO观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled
基本使用
NSOperation 单独使用时系统同步执行操作,配合 NSOperationQueue 我们能更好的实现异步执行
使用步骤分为三步
- 创建操作:将需要执行的操作封装到一个NSOperation对象中
- 创建队列:创建NSOperationQueue对象
- 将操作加入到队列中:将NSOperation对象添加到NSOperationQueue对象中
之后,系统将队列中的操作取出来执行操作
创建操作
上面说了,我们不能直接使用抽象类,而是使用其子类或系统定义的子类
使用子类NSInvocationOperation(Invocation:调用)
// 1.创建 NSInvocationOperation 对象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 2.调用 start 方法开始执行操作
[op start];
- (void)task1 {
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
可以看到,操作实在当前线程执行的,没有开启新线程
我们使用NSThread创建线程后自启动新线程
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
在新线程执行操作,结果就是其他线程
也就意味着其只能在自己当前所在的线程中执行同步操作
使用子类NSBlockOperation
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 2.调用 start 方法开始执行操作
[op start];
一样 也是在当前线程执行同步操作,在其他线程操作则为其他线程
但是其提供了一个方法
addExecutionBlock:^
可以为NSBlockOperation添加额外的操作
这些操作可以在不同的线程并发执行
使用自定义继承自NSOperation的子类
如果使用系统提供的两个子类不能满足日常需求,我们可以使用自定义继承自NSOperation的子类。可以通过重写main或者start方法来定义自己的NSOperation对象
重写main方法
NSOperationTest *op = [[NSOperationTest alloc] init];
[op start];
自定义的Test类中重写main方法
- (void)main {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
}
}
重写start方法
- (void)start {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
}
[super start];
}
一个道理
创建队列
- 主队列:在主线程中执行
// 主队列获取方法
NSOperationQueue *queue = [NSOperationQueue mainQueue];
- 自定义队列:自动放到子线程中执行,同时包含了串行、并发的功能?(什么串行???)
// 自定义队列创建方法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
将操作加入到队列中
两种方法
-
addOperation
:将操作加入队列中能够开启新线程,进行并发执行两个任务
-
addOperationWithBlock:
将操作加入到操作队列后能够开启新线程,进行并发执行
NSOperationQueue控制串行、并发
NSOperationQueue
有个属性maxConcurrentOperationCount
,最大并发数
用来控制一个队列中可以有多少个操作同时参与并发
- 关于最大并发数
maxConcurrentOperationCount
maxConcurrentOperationCount
默认为-1,表示可以并发执行maxConcurrentOperationCount
为1时,串行队列,只能串行执行maxConcurrentOperationCount
大于1,队列为并发队列,
NSOperation操作依赖
我们可以添加操作之间的依赖关系,通过操作依赖,我们可以很方便的控制操作之间的操作先后顺序
addDependency:(NSOperation *)op;
添加依赖,使当前操作依赖于操作op的完成removeDependency:(NSOperation *)op;
移除依赖,取消当前操作对操作op的依赖@property (readonly, copy) NSArray <NSOperation *> * dependencies
在当前操作开始执行之前完成执行的所有操作对象数组
使用addOperations
@property (readonly, copy) NSArray <NSOperation *> * dependencies;
waitUntilFinished为YES
数组中的两个线程并发进行
第三个等待前两个结束之后执行 并且第三个使用的是数组中的最后一个线程
为NO
不等待 三个新线程
NSOperation优先级
NSOperation提供了优先级属性(queuePriority
)。
我们可以通过setQueuePriority
方法来改变当前操作在统一队列中的执行优先级
// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
}
前面提到过,对于添加到队列中的操作,首先进入准备就绪的状态,就绪状态取决于操作之间的依赖关系,然后进入就绪状态的操作的开始执行顺序由操作之间的优先级确定(优先级是操作对象自身的属性)
优先级不能取代依赖关系。这也就意味着
假如一个队列中既包含了准备就绪状态的操作(没有任何依赖关系,其就是第一个),又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高,那么虽然准备就绪的操作优先级低,也会优先执行。
也就是官方文档中的:
仅当需要对非相关操作的相对优先级进行分类时,才应使用优先级值。优先级值不应用于实现不同操作对象之间的依赖关系管理
如果优先级一样 就按添加顺序执行(最大线程数为1)
为什么第一个优先级设置为Low没用,为什么第一个固定与addOperation添加顺序的第一个有关呢
说好的不看添加顺序呢?
NSOperation、NSOperationQueue线程间的通讯
回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//需要在主线程执行的代码
}];
线程安全
NSLock来加锁
- 初始化NSLock对象
self.lock = [[NSLock alloc] init]
- 上锁
[self.lock lock]
- 解锁
[self.lock unlock]
常用属性和方法
NSOperation
- 取消操作
cancel
- 判断操作状态方法
-
isFinished
判断操作是否已经结束
-
isCancelled
判断操作是否取消
-
isExecuting
判断操作是否正在运行
-
isReady
判断是否处于就绪状态
- 操作同步
-
waitUntilFinished
阻塞当前线程
-
setCompletionBlock:(void (^)(void))block;
当前操作完毕之后执行block
-
addDependency
添加依赖
-
removeDependency
移除依赖
-
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
数组存储操作
NSOperationQueue
- 取消/暂停/恢复队列中的操作
-
cancelAllOperations;
取消队列中所有的操作
-
isSuspended
判断队列是否处理暂停状态 YES:暂停状态,NO恢复状态
-
setSuspended:(BOOL)b;
设置操作的暂停和恢复 YES:暂停,NO:恢复
- 同步操作
-
waitUntilAllOperationsAreFinished;
阻塞当前线程,直到队列中的操作全部完成
- 添加/获取
-
addOperationWithBlock:(void (^)(void))block
向队列中添加一个NSBlockOperation类型的操作对象
-
addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait;
添加操作数组,wait标志是否阻塞当前线程知道所有操作结束
-
operations
当前在队列中的操作数组
-
operations
操作个数
- 获取队列
-
currentQueue
当前队列,如果当前线程不在Queue上,返回nil
-
mainQueue
获取主队列
多线程的比较
- pthread
pthread跨平台,使用难度大,需要手动管理线程生命周期 - GCD与NSOperation
- GCD的执行效率更高,执行的是由Block构成的任务,是一个轻量级的数据结构,写起来更加方便
- GCD只支持FIFO队列,NSOperation可以通过设置最大并发数、设置优先级、添加依赖关系来调整执行顺序
- NSOperation可以跨越队列设置依赖关系,GCD仅仅能通过栅栏等方法才能控制执行顺序
- NSOperation更加面向对象,支持KVO,也可以通过继承等关系添加子类。
所以如果我们需要考虑异步操作之间的顺序行、依赖关系,比如多线程并发下载等等,就使用NSOperation
- GCD 与 NSThread 的区别
- NSThread 通过 @selector 指定要执行的方法,代码分散, 依靠的是NSObject的分类实现的线程之间的通讯,如果要开线程必须创建多个线程对象。经常只用的是[NSTread current] 查看当前的线程。
- NSThread是一个控制线程执行的对象,它不如NSOperation抽象,通过它我们可以方便的得到一个线程,并控制它。但NSThread的线程之间的并发控制,是需要我们自己来控制的,可以通过NSCondition实现。
- GCD 通过 block 指定要执行的代码,代码集中, 所有的代码写在一起的,让代码更加简单,易于阅读和维护,不需要管理线程的创建/销毁/复用的过程!程序员不用关心线程的生命周期
多线程的优缺点
优点:
使应用程序的响应速度更快,用户界面在进行其他工作的同时仍始终保持活动状态;优化任务执行,适当提高资源利用率(CPU,内存)
缺点:
线程占用内存空间,管理线程需要额外CPU开销,开启大量线程,降低程序性能;增加程序复杂度,如线程间通信,多线程的资源共享等等