iOS多线程-NSOperation, NSOperationQueue
1.NSOperation、NSOperationQueue 简介
-
NSOperation
,NSOperationQueue
是对GCD的包装 - 两个核心概念【队列+操作】
2.操作(NSOperation
)和队列(NSOperationQueue
)
2.1.操作NSOperation
- 执行操作的意思,换句话说就是你在线程中执行的任务.
-
NSOperation
是个抽象类,并不具备封装操作的能力,必须使用它的子类. - 使用NSOperation子类的方式有3种
NSInvocationOperation
NSBlockOperation
- 自定义子类继承
NSOperation
,实现内部相应的方法
2.2.队列NSOperationQueue
- 单独使用
NSOperation
也可以开启新的线程(由系统决定),但只能串行执行,要想实现多条线程异步执行就必须将操作添加到NSOperationQueue
队列一起使用. -
NSOperationQueue
对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性. - 操作队列通过设置最大并发操作数(
maxConcurrentOperationCount
)来控制并发、串行. - NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列中的人物在主线程上执行,而自定义队列在后台执行。
2.3. NSOperation、NSOperationQueue基本使用
1.单独使用NSOperation
- 使用步骤:
创建操作对象,将任务封装在操作中,调用start
开始执行任务 .
1.使用子类 NSInvocationOperation
//子类NSInvocationOperation
- (void)invocationOperation{
NSInvocationOperation * inop1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(task) object:nil];//创建操作对象封装task任务
NSInvocationOperation * inop2 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(task) object:nil];//创建操作对象封装task任务
NSInvocationOperation * inop3 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(task) object:nil];//创建操作对象封装task任务
[inop1 start];//执行操作
[inop2 start];
[inop3 start];
}
-(void)task{
NSLog(@"%@",[NSThread currentThread]);
}
结果可见:并没有开启新的线程,在当前线程串行执行了任务.
2.使用子类 NSBlockOperation
NSBlockOperation
里面只封装一个任务时,效果同NSInvocationOperation
一样,不会开启新的线程,只在当前线程中串行执行任务,而NSBlockOperation
提供了addExecutionBlock:
可以在NSBlockOperation
操作对象中添加额外的任务,这时候系统会开启新的线程,但只能串行执行.
//子类NSBlockOperation
- (void)blockOperation{
NSBlockOperation * blockop = [NSBlockOperation blockOperationWithBlock:^{//创建操作对象并在block中封装任务
NSLog(@"任务1:%@",[NSThread currentThread]);
}];
[blockop addExecutionBlock:^{//添加额外的任务到操作对象中
NSLog(@"任务2:%@",[NSThread currentThread]);
}];
[blockop addExecutionBlock:^{
NSLog(@"任务3:%@",[NSThread currentThread]);
}];
[blockop addExecutionBlock:^{
NSLog(@"任务4:%@",[NSThread currentThread]);
}];
[blockop addExecutionBlock:^{
NSLog(@"任务5:%@",[NSThread currentThread]);
}];
[blockop start];;//执行操作
}
结果可见:在添加额外的任务后,系统开启了多条线程执行任务,但是是串行执行的.一般情况下,NSBlockOperation
是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。
3.继承NSOperation
的自定义子类
还可以使用自定义继承自 NSOperation
的子类。可以通过重写 main
方法封装要执行的任务。当 main
执行完返回的时候,这个操作就结束了。
//头文件
#import <Foundation/Foundation.h>
@interface JHOperation : NSOperation
@end
//实现文件
#import "JHOperation.h"
@implementation JHOperation
- (void)main{
NSLog(@"JHOperation:%@",[NSThread currentThread]);
}
@end
//引入其他文件调用
//自定义继承NSOperation的子类
- (void)customSunOperation{
JHOperation * jhop1 = [[JHOperation alloc]init];
JHOperation * jhop2 = [[JHOperation alloc]init];
JHOperation * jhop3 = [[JHOperation alloc]init];
[jhop1 start];
[jhop2 start];
[jhop3 start];
}
结果可见:单独使用的话,并没有开启新的线程,串行执行.
2.操作(NSOperation)和队列(NSOperationQueue)配合使用
- NSOperationQueue 一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。
- 凡是添加到主队列中的操作,都会放到主线程中执行。
- 自定义队列(非主队列),添加到这种队列中的操作,就会自动放到子线程中执行,是并发还是串行执行由系统决定.
//获取主队列
NSOperationQueue *queue = [NSOperationQueue mainQueue];
//创建自定义队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
- 使用步骤:
- 创建
NSOperationQueue
队列; - 创建
NSOperation
操作对象,将执行的操作封装到一个NSOperation
对象中; - 然后将
NSOperation
对象添加到NSOperationQueue
队列中,系统会自动将NSOperationQueue
中的NSOperation
取出来,将取出的NSOperation
封装的操作放到一条新线程中执行.
- 创建
- 将创建好的操作加入到队列中去。总共有两种方法:
-
- (void)addOperation:(NSOperation *)op;
需要创建操作,将任务封装在操作中,将操作添加到队列中执行,在添加的同时操作会自动调start
方法。 -
- (void)addOperationWithBlock:(void (^)(void))block;
,无需先创建操作,在 block 中添加操作,直接将要执行的任务 封装在block中,然后添加到队列中.
-
//1.使用 addOperation: 将操作加入到操作队列中
-(void)addOperationToQueue{
NSOperationQueue * queue = [[NSOperationQueue alloc]init];//创建队列
NSInvocationOperation * op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(task:) object:@"任务1"];//创建操作1
NSBlockOperation * op2 = [NSBlockOperation blockOperationWithBlock:^{
[self task:@"任务2"];
}];//创建操作2
JHOperation * op3 = [[JHOperation alloc]init];//创建操作3
// 使用 addOperation: 添加所有操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
}
-(void)task:(NSString *)object{
NSLog(@"%@:%@",object,[NSThread currentThread]);
}
//自定义继承`NSOperation`的子类
#import "JHOperation.h"
@implementation JHOperation
- (void)main{
NSLog(@"JHOperation:%@",[NSThread currentThread]);
}
@end
结果可见:系统开启了多个线程并且并发的执行了任务.
//2.使用 addOperationWithBlock: 将操作加入到操作队列中
-(void)addOperationWithBlockToQueue{
NSOperationQueue * queue = [[NSOperationQueue alloc]init];//创建操作队列
//添加block任务到队列中
[queue addOperationWithBlock:^{
NSLog(@"任务1:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"任务2:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"任务3:%@",[NSThread currentThread]);
}];
}
结果可见:系统开启了多个线程并且并发的执行了任务.
3.NSOperationQueue设置最大并发数来控制串行还是并发执行
- 要想真正实现有线的串行和并发执行任务,我们就需要设置
NSOperationQueue
的maxConcurrentOperationCount
(0最大并发数)属性来进行控制,当然这里的最大并发数并不是我们能人为的设置开启的线程数,真正能开启几个线程由系统决定.-
maxConcurrentOperationCount
默认情况下为-1,表示不进行限制,可进行并发执行。 -
maxConcurrentOperationCount
为0时,不会执行任务。 -
maxConcurrentOperationCount
为1时,队列为串行队列。只能串行执行。 -
maxConcurrentOperationCount
大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统能允许的最大值。
-
// 设置 MaxConcurrentOperationCount(最大并发操作数来控制并发串行执行)
-(void)setMaxConcurrentOperationCount{
NSOperationQueue * queue = [[NSOperationQueue alloc]init];//创建队列
queue.maxConcurrentOperationCount = 1;//串行执行
// queue.maxConcurrentOperationCount = 2;//并发执行
// queue.maxConcurrentOperationCount = 6;//并发执行
//添加操作
[queue addOperationWithBlock:^{
NSLog(@"任务1:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"任务2:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"任务3:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"任务4:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"任务5:%@",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"任务6:%@",[NSThread currentThread]);
}];
}
maxConcurrentOperationCount
设置为1时的结果:
结果可见:当最大并发数设置为1时,开启了2条新的线程,但是任务是串行执行的. maxConcurrentOperationCount
设置为2时的结果:
结果可见:当最大并发数设置为2或6时,开启了多条线程且并发执行了任务.
4.NSOperation
的操作依赖
- 通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。
NSOperation
提供了3个接口供我们管理和查看依赖。-
- (void)addDependency:(NSOperation *)op;
添加依赖,使当前操作依赖于操作 op 的完成。 -
- (void)removeDependency:(NSOperation *)op;
移除依赖,取消当前操作对操作 op 的依赖。 -
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
在当前操作开始执行之前完成执行的所有操作对象数组。
-
//操作依赖:addDependency
- (void)operationAddDependency{
NSOperationQueue * queue = [[NSOperationQueue alloc]init];//创建队列
//创建操作
NSBlockOperation * op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务1:%@",[NSThread currentThread]);
}];
NSBlockOperation * op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务2:%@",[NSThread currentThread]);
}];
NSBlockOperation * op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务3:%@",[NSThread currentThread]);
}];
//添加依赖
[op1 addDependency:op3];
[op3 addDependency:op2];
//将操作添加到队列中并执行
NSArray * opArray = [NSArray arrayWithObjects:op1,op2,op3, nil];
[queue addOperations:opArray waitUntilFinished:NO];
}
结果可见:执行任务顺序任务2,任务3,任务1; 注意
:不能互相添加依赖,可以跨队列依赖
5.线程间的通信
我们一般在主线程里边进行 UI 刷新,在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通信。
- (void)communication{
NSOperationQueue * queue = [[NSOperationQueue alloc]init];//创建队列
//添加耗时操作
[queue addOperationWithBlock:^{
for (int i = 0; i < 6; i++) {
NSLog(@"耗时操作:%@",[NSThread currentThread]);
}
//执行完操作后回到主线程刷新UI
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
NSLog(@"刷新UI:%@",[NSThread currentThread]);
self.view.backgroundColor = [UIColor blueColor];
}];
}];
}
6.NSOperation
、NSOperationQueue
线程安全
- 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。如果对跳线程都同一块之源进行过增删改都操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
- 线程安全解决方案:可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/ge等等各种方式。
- 我们模拟火车票售卖的方式,实现 NSOperation 线程安全和解决线程同步问题。场景:总共有20张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。
1.线程不安全示例
static NSInteger totalNum;
-(void)initUnsafeTicketStatus{
totalNum = 20;
//创建队列代表北京售票窗口
NSOperationQueue * beijingQueue = [[NSOperationQueue alloc]init];
beijingQueue.maxConcurrentOperationCount = 1;//设置最大并发数为1(串行执行)
//创建队列代表上海售票窗口
NSOperationQueue * shanghaiQueue = [[NSOperationQueue alloc]init];
shanghaiQueue.maxConcurrentOperationCount = 1;
//北京,上海两个窗口都执行售票操作
__weak typeof(self)weakSelf = self;
NSBlockOperation * sale1 = [NSBlockOperation blockOperationWithBlock:^{
//北京售票口可以执行售票任务
[weakSelf unsafeSaleTicketsWithWindow:@"北京"];
}];
NSBlockOperation * sale2 = [NSBlockOperation blockOperationWithBlock:^{
//上海售票口封装售票任务
[weakSelf unsafeSaleTicketsWithWindow:@"上海"];
}];
//将操作添加到队列中执行
[beijingQueue addOperation:sale1];
[shanghaiQueue addOperation:sale2];
}
-(void)unsafeSaleTicketsWithWindow:(NSString *)windowName{
while (1) {
if (totalNum) {
// 每卖一张票,总票数就少一张
totalNum -- ;
NSLog(@"%@窗口出售了一张票(线程:%@),还剩%zd张票",windowName,[NSThread currentThread],totalNum);
}else{
NSLog(@"票已经卖完");
break;
}
}
}
结果可见:开始和结束都执行了两次,线程是不安全的。
2.线程安全示例
static NSInteger totalNum;
static NSLock * lock;
- (void)initSafeTicketStatus{
totalNum = 20;
//创建锁对象
lock = [[NSLock alloc]init];
NSOperationQueue * queue1 = [[NSOperationQueue alloc]init];//代表北京窗口
queue1.maxConcurrentOperationCount = 1;
NSOperationQueue * queue2 = [[NSOperationQueue alloc]init];//代表上哈窗口
queue1.maxConcurrentOperationCount = 1;
__weak typeof(self)weakSelf = self;
NSBlockOperation * sale1 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf safeSaleTicketsWithWindow:@"北京"];
}];
NSBlockOperation * sale2 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf safeSaleTicketsWithWindow:@"上海"];
}];
[queue1 addOperation:sale1];
[queue2 addOperation:sale2];
[sale2 setCompletionBlock:^{
}];
}
//售火车票
-(void)safeSaleTicketsWithWindow:(NSString *)windowName{
while (YES) {
[lock lock];//加锁
if (totalNum) {
// 每卖一张票,总票数就少一张
totalNum -- ;
NSLog(@"%@窗口出售了一张票(线程:%@),还剩%zd张票",windowName,[NSThread currentThread],totalNum);
}else{
NSLog(@"票已经卖完");
break;
}
[lock unlock];//解锁
}
}
结果可见:给多个线程并发访问的同一块资源加锁可以保证同时只有一个线程能访问该资源,即同步执行来保证线程安全。
7.NSOperation
、NSOperationQueue
常用属性和方法归纳
-
NSOperation
常用属性和方法- 开始执行操作
- (void)start;
- 自定义继承
NSOperation
的子类,在main
方法中添加要执行的任务- (void)main;
- 取消操作方法
- (void)cancel;
可取消操作,实质是标记 isCancelled 状态。 - 判断操作状态方法
- (BOOL)isFinished;
判断操作是否已经结束。- (BOOL)isCancelled;
判断操作是否已经标记为取消。- (BOOL)isExecuting;
判断操作是否正在在运行。- (BOOL)isReady;
判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关。 - 依赖,监听,同步等操作
- (void)waitUntilFinished;
阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步。@property (nullable, copy) void (^completionBlock)(void)
监听当前操作完成然后执行完成该操作后的block。- (void)addDependency:(NSOperation *)op;
添加依赖,使当前操作依赖于操作 op 的完成。- (void)removeDependency:(NSOperation *)op;
移除依赖,取消当前操作对操作 op 的依赖。@property (readonly, copy) NSArray<NSOperation *> *dependencies;
在当前操作开始执行之前
完成执行的所有操作对象数组。 - NSOperation 优先级
(1)NSOperation
提供了queuePriority
(优先级)属性,queuePriority
属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal
。但是我们可以通过setQueuePriority
:方法来改变当前操作在同一队列中的执行优先级。进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
(2)当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行,此时优先级才生效。即一个队列中的操作同时设置了依赖和优先级,未准备就绪的操作优先级比准备就绪的操作优先级高。虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。。
- 开始执行操作
// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,//优先级最低
NSOperationQueuePriorityLow = -4L,//优先级较低
NSOperationQueuePriorityNormal = 0,//默认优先级
NSOperationQueuePriorityHigh = 4,//优先级较高
NSOperationQueuePriorityVeryHigh = 8//优先级最高
};
-
NSOperationQueue
常用属性和方法- 获取主队列
@property (class, readonly, strong) NSOperationQueue *mainQueue
- 获取当前队列
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue
- 取消/暂停/恢复操作
- (void)cancelAllOperations;
取消队列中所有操作- (void)setSuspended:(BOOL)b;
可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列。- (BOOL)isSuspended;
判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态。 - 添加/获取操作
- (void)addOperationWithBlock:(void (^)(void))block;
向队列中添加block任务的操作- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait
向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
获取队列中的操作数组。@property (readonly) NSUInteger operationCount
获取队列中的操作数。@property NSInteger maxConcurrentOperationCount;
设置最大并发数来控制并发/串行执行。- (void)waitUntilAllOperationsAreFinished;
阻塞当前线程,直到队列中的操作全部执行完毕。
- 获取主队列
-
注意:
- 这里的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
- 暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。
参考资料:
苹果官方文档—并发编程指南
苹果官方文档:NSOperation
并发编程:API 及挑战
iOS多线程相关文章:
iOS多线程简述
iOS多线程-pthread、NSThread
iOS多线程-GCD
iOS多线程-NSOperation, NSOperationQueue
iOS多线程-RunLoop
OC单例模式详解