每日一问13——多线程之NSOperation与NSOperat
NSOperation
看一下官方的介绍:
The NSOperation class itself is an abstract base class that must be subclassed in order to do any useful work.
NSOperation
是一个抽象基类,我们需要使用它的子类来完成工作。从使用上来说NSOperation
就是对GCD
中block
的封装,它表示一个要被执行的任务。
与GCD
相比,NSOperation
可以更好的控制任务,包括暂停,继续,取消这类操作并且提供了当前任务的状态供开发者监听。而在GCD
中要实现这些需求可能会需要其他很多复杂的代码。
NSOperation基本功能
1.基本的操作和状态
先看一下NSOperation.h中的NSOperation类。
- (void)start;
- (void)main;
@property (readonly, getter=isCancelled) BOOL cancelled;
- (void)cancel;
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below
@property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);
@property (readonly, getter=isReady) BOOL ready;
NSOperation提供了任务的状态,执行,完毕,取消等。并且提供了一个start方法,这表明NSOperation是可以直接执行的。这里要注意NSOperation执行是一个同步操作,任务会执行在当前运行的线程上。
2.依赖关系
- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
NSOperation还提供了一个依赖功能,通过依赖可以控制任务执行的顺序。
3.任务优先级
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
@property NSOperationQueuePriority queuePriority;
与GCD
不同,GCD
只能设置队列的优先级,而NSOperation
及其子类可以设置任务的优先级。需要注意的是,NSOperationQueue
也不能完全保证优先级高的任务一定先执行。
系统提供的子类
NSInvocationOperation
这个类的使用类似于给UIbutton添加一个方法。需要一个对象和一个Selector。
NSInvocationOperation *inOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testInOp) object:nil];
NSLog(@"begin");
[inOp start];
NSLog(@"end");
- (void)testInOp {
NSLog(@"%@",[NSThread currentThread]);
}
打印结果:
2017-09-19 16:12:55.938 learn_09_NSOperation[3507:146749] begin
2017-09-19 16:12:55.938 learn_09_NSOperation[3507:146749] <NSThread: 0x60000006e700>{number = 1, name = main}
2017-09-19 16:12:55.939 learn_09_NSOperation[3507:146749] end
可以看出,这个任务相是同步执行的。再修改一下例子:
NSInvocationOperation *inOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testInOp) object:nil];
NSLog(@"begin");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[inOp start];
});
NSLog(@"end");
打印结果
2017-09-19 16:14:44.266 learn_09_NSOperation[3536:147731] begin
2017-09-19 16:14:44.266 learn_09_NSOperation[3536:147731] end
2017-09-19 16:14:44.267 learn_09_NSOperation[3536:147761] <NSThread: 0x60000007c240>{number = 3, name = (null)}
可以看出,任务可以直接在子线程启动,就变成一个异步执行的任务。其实这里还是在其他线程中同步执行了该任务
NSBlockOperation
与之前的NSInvocationOperation比较,NSBlockOperation在使用上更加的方便,它提供一个block代码块来执行任务。
NSBlockOperation *bOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];
[bOp start];
执行结果:<NSThread: 0x60800006f280>{number = 1, name = main}
我们可以看出基本使用上与NSInvocationOperation结果一致。但不同的是NSBlockOperation支持了并发执行一个或多个block。
NSBlockOperation *bOp = [[NSBlockOperation alloc] init];
[bOp addExecutionBlock:^{
NSLog(@"1--%@",[NSThread currentThread]);
NSLog(@"2--%@",[NSThread currentThread]);
NSLog(@"3--%@",[NSThread currentThread]);
}];
[bOp addExecutionBlock:^{
NSLog(@"4--%@",[NSThread currentThread]);
NSLog(@"5--%@",[NSThread currentThread]);
NSLog(@"6--%@",[NSThread currentThread]);
}];
[bOp addExecutionBlock:^{
NSLog(@"7--%@",[NSThread currentThread]);
NSLog(@"8--%@",[NSThread currentThread]);
NSLog(@"9--%@",[NSThread currentThread]);
}];
[bOp start];
执行结果:
2017-09-19 16:22:53.392 learn_09_NSOperation[3631:152054] 1--<NSThread: 0x60800007ad80>{number = 1, name = main}
2017-09-19 16:22:53.392 learn_09_NSOperation[3631:152054] 2--<NSThread: 0x60800007ad80>{number = 1, name = main}
2017-09-19 16:22:53.392 learn_09_NSOperation[3631:152054] 3--<NSThread: 0x60800007ad80>{number = 1, name = main}
2017-09-19 16:22:53.392 learn_09_NSOperation[3631:152091] 4--<NSThread: 0x60800007c440>{number = 3, name = (null)}
2017-09-19 16:22:53.392 learn_09_NSOperation[3631:152089] 7--<NSThread: 0x608000266300>{number = 4, name = (null)}
2017-09-19 16:22:53.393 learn_09_NSOperation[3631:152091] 5--<NSThread: 0x60800007c440>{number = 3, name = (null)}
2017-09-19 16:22:53.393 learn_09_NSOperation[3631:152089] 8--<NSThread: 0x608000266300>{number = 4, name = (null)}
2017-09-19 16:22:53.394 learn_09_NSOperation[3631:152091] 6--<NSThread: 0x60800007c440>{number = 3, name = (null)}
2017-09-19 16:22:53.394 learn_09_NSOperation[3631:152089] 9--<NSThread: 0x608000266300>{number = 4, name = (null)}
从中我们可以看出每一个代码块执行在一条线程上,每条线程之间彼此无关,任务执行顺序是并发执行的。
为了更好的操作这些任务,我们需要结合NSOperationQueue
来管理这些任务。这样,我们任务
与队列
的概念就结合起来了。
自定义NSOperation的子类
自定义串行NSOperation,只需要在子类中重写main
函数
自定义并行NSOperation,需要子类实现start
,main
,isExecuting
,isFinished
,isConcurrent
,isAsynchronous
方法
一般来说我们使用系统提供的子类就可以完成绝大部分需求了。关于自定义子类可参见这篇文章NSOperation 自定义子类实现非并发和并发操作
NSOperationQueue
1.队列的基本使用
NSOperationQueue
类似于GCD
中的队列。我们知道GCD
中的队列有三种:主队列
、串行队
列和并发队列
。NSOperationQueue
更简单,只有两种:主队列
和非主队列
。
默认情况下,我们创建的NSOperationQueue队列都是非主队列。
我们可以通过NSOperationQueue.mainQueue
来获取主队列。
NSOperationQueue
的主队列是串行队列,而且其中所有NSOperation
都会在主线程中执行。
对于非主队列来说,一旦一个NSOperation
被放入其中,那这个NSOperation
一定是并发执行的。因为NSOperationQueue
会为每一个NSOperation
创建线程并调用它的start
方法。
但这并不意味着NSOperationQueue
不支持串行执行。NSOperationQueue
提供了maxConcurrentOperationCount
属性,让我们设置队列中的最大并发数。当设置为1时,队列会按串行方式执行里面的任务。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
for (int i=0; i < 10; ++i) {
NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d----%@",i,[NSThread currentThread]);
}];
[queue addOperation:bop];
}
打印结果:
2017-09-19 16:38:09.401 learn_09_NSOperation[3763:158562] 0----<NSThread: 0x608000079500>{number = 3, name = (null)}
2017-09-19 16:38:09.402 learn_09_NSOperation[3763:158562] 1----<NSThread: 0x608000079500>{number = 3, name = (null)}
2017-09-19 16:38:09.402 learn_09_NSOperation[3763:158562] 2----<NSThread: 0x608000079500>{number = 3, name = (null)}
2017-09-19 16:38:09.403 learn_09_NSOperation[3763:158562] 3----<NSThread: 0x608000079500>{number = 3, name = (null)}
2017-09-19 16:38:09.403 learn_09_NSOperation[3763:158562] 4----<NSThread: 0x608000079500>{number = 3, name = (null)}
2017-09-19 16:38:09.405 learn_09_NSOperation[3763:158562] 5----<NSThread: 0x608000079500>{number = 3, name = (null)}
2017-09-19 16:38:09.405 learn_09_NSOperation[3763:158562] 6----<NSThread: 0x608000079500>{number = 3, name = (null)}
2017-09-19 16:38:09.406 learn_09_NSOperation[3763:158562] 7----<NSThread: 0x608000079500>{number = 3, name = (null)}
2017-09-19 16:38:09.406 learn_09_NSOperation[3763:158562] 8----<NSThread: 0x608000079500>{number = 3, name = (null)}
2017-09-19 16:38:09.407 learn_09_NSOperation[3763:158562] 9----<NSThread: 0x608000079500>{number = 3, name = (null)}
从结果可以看出,在最大并发为1的情况下,队列是异步的串行执行的。当然如果我们不设置最大并发数或者>1的情况下,系统会自动为每个任务分配线程,所有的线程调度都交给系统控制。
2.任务管理
由于我们的NSOperation对象拥有自己的任务状态,为了管理队列中的NSOperation任务,系统为我们提供了以下方法来获取队列中的任务。
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
我们可以获取到队列中的每一个任务并操作它。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
for (int i=0; i < 10; ++i) {
NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d----%@",i,[NSThread currentThread]);
}];
[queue addOperation:bop];
}
NSBlockOperation *op = queue.operations.lastObject;
NSLog(@"%d",op.isCancelled);
[op cancel];
NSLog(@"%d",op.isCancelled);
在执行前将任务取消掉,则该任务将不会被执行。
当然,在任务较多的情况下,这样取消任务实在是太可爱了。NSOperationQueue 提供了cancelAllOperations
方法,帮我们直接取消掉所有的任务。
3.其他操作
队列优先级
像CGD一样,NSOperationQueue同样提供了优先级设置,我们可以根据不同场景选择对应的优先级。
typedef NS_ENUM(NSInteger, NSQualityOfService) {
/* UserInteractive QoS is used for work directly involved in providing an interactive UI such as processing events or drawing to the screen. */
NSQualityOfServiceUserInteractive = 0x21,
/* UserInitiated QoS is used for performing work that has been explicitly requested by the user and for which results must be immediately presented in order to allow for further user interaction. For example, loading an email after a user has selected it in a message list. */
NSQualityOfServiceUserInitiated = 0x19,
/* Utility QoS is used for performing work which the user is unlikely to be immediately waiting for the results. This work may have been requested by the user or initiated automatically, does not prevent the user from further interaction, often operates at user-visible timescales and may have its progress indicated to the user by a non-modal progress indicator. This work will run in an energy-efficient manner, in deference to higher QoS work when resources are constrained. For example, periodic content updates or bulk file operations such as media import. */
NSQualityOfServiceUtility = 0x11,
/* Background QoS is used for work that is not user initiated or visible. In general, a user is unaware that this work is even happening and it will run in the most efficient manner while giving the most deference to higher QoS work. For example, pre-fetching content, search indexing, backups, and syncing of data with external systems. */
NSQualityOfServiceBackground = 0x09,
/* Default QoS indicates the absence of QoS information. Whenever possible QoS information will be inferred from other sources. If such inference is not possible, a QoS between UserInitiated and Utility will be used. */
NSQualityOfServiceDefault = -1
} NS_ENUM_AVAILABLE(10_10, 8_0);
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);
等待执行
- (void)waitUntilAllOperationsAreFinished
NSOperationQueue提供了这样一个函数,让调用该函数前的所有任务执行完毕以后才会执行后续的操作
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
for (int i=0; i < 3; ++i) {
NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d----%@",i,[NSThread currentThread]);
}];
[queue addOperation:bop];
}
NSLog(@"wait");
[queue waitUntilAllOperationsAreFinished];
NSLog(@"go on");
打印结果:
2017-09-19 16:58:22.864 learn_09_NSOperation[3908:166871] wait
2017-09-19 16:58:22.865 learn_09_NSOperation[3908:166906] 1----<NSThread: 0x6000002620c0>{number = 3, name = (null)}
2017-09-19 16:58:22.865 learn_09_NSOperation[3908:166907] 2----<NSThread: 0x608000078940>{number = 5, name = (null)}
2017-09-19 16:58:22.865 learn_09_NSOperation[3908:166909] 0----<NSThread: 0x600000262600>{number = 4, name = (null)}
2017-09-19 16:58:22.865 learn_09_NSOperation[3908:166871] go on
当任务执行完毕后,才会继续执行waitUntilAllOperationsAreFinished
下面的代码,可以看出waitUntilAllOperationsAreFinished
会阻塞当前的线程来等待队列中任务执行。
暂停队列
NSOperationQueue 提供了suspended
的属性,顾名思义这个属性用来控制队列是否暂停。但它的暂停方法并不是那么直接。官方文档上是这么说的:
如果这个值设置为 NO,那说明这个队列已经准备好了可以执行了。如果这个值设置为 YES,那么已经添加到队列中的操作还是可以执行了,而后面继续添加进队列中的操作才处于暂停状态,直到你再次将这个值设置为 NO 时,后面加入的操作才会继续执行。这个属性的默认值是 NO。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
for (int i=0; i < 4; ++i) {
NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d",i);
}];
if(i == 2) {
[queue setSuspended:YES];
}
[queue addOperation:bop];
}
//模拟耗时操作后开启队列
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"re start");
[queue setSuspended:NO];
});
打印结果:
2017-09-19 17:10:23.796 learn_09_NSOperation[3994:171960] 0
2017-09-19 17:10:23.796 learn_09_NSOperation[3994:171959] 1
2017-09-19 17:10:25.796 learn_09_NSOperation[3994:171922] re start
2017-09-19 17:10:25.796 learn_09_NSOperation[3994:171959] 2
2017-09-19 17:10:25.796 learn_09_NSOperation[3994:171960] 3
一个例子:利用任务依赖实现一个规定顺序的任务队列
假设我们10个任务,我们需要异步执行,先顺序执行后5条任务再顺序执行前5条任务。通过NSOperation的依赖功能与NSOperationQueue结合来满足这个需求。(代码写得不好,只是为了表现NSOperationQueue的使用灵活与简便)
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSMutableArray *ops = [NSMutableArray array];
for (int i = 0; i < 10; ++i) {
NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d---%@",i,[NSThread currentThread]);
}];
[ops addObject:bop];
}
//
[ops enumerateObjectsUsingBlock:^(NSBlockOperation *obj, NSUInteger idx, BOOL * _Nonnull stop) {
if(idx > 0 && idx < 5){
NSBlockOperation *old = ops[idx - 1];
[obj addDependency:old];
}
else if (idx > 5) {
NSBlockOperation *old = ops[idx - 1];
[obj addDependency:old];
}
if(idx == ops.count - 1) {
NSBlockOperation *fst = ops.firstObject;
[fst addDependency:obj];
}
}];
[queue addOperations:ops waitUntilFinished:NO];
打印结果:
2017-09-19 17:16:33.256 learn_09_NSOperation[4037:174132] 5---<NSThread: 0x608000267c80>{number = 3, name = (null)}
2017-09-19 17:16:33.258 learn_09_NSOperation[4037:174132] 6---<NSThread: 0x608000267c80>{number = 3, name = (null)}
2017-09-19 17:16:33.260 learn_09_NSOperation[4037:174132] 7---<NSThread: 0x608000267c80>{number = 3, name = (null)}
2017-09-19 17:16:33.261 learn_09_NSOperation[4037:174134] 8---<NSThread: 0x608000262d80>{number = 4, name = (null)}
2017-09-19 17:16:33.262 learn_09_NSOperation[4037:174134] 9---<NSThread: 0x608000262d80>{number = 4, name = (null)}
2017-09-19 17:16:33.265 learn_09_NSOperation[4037:174134] 0---<NSThread: 0x608000262d80>{number = 4, name = (null)}
2017-09-19 17:16:33.283 learn_09_NSOperation[4037:174134] 1---<NSThread: 0x608000262d80>{number = 4, name = (null)}
2017-09-19 17:16:33.292 learn_09_NSOperation[4037:174134] 2---<NSThread: 0x608000262d80>{number = 4, name = (null)}
2017-09-19 17:16:33.298 learn_09_NSOperation[4037:174132] 3---<NSThread: 0x608000267c80>{number = 3, name = (null)}
2017-09-19 17:16:33.299 learn_09_NSOperation[4037:174134] 4---<NSThread: 0x608000262d80>{number = 4, name = (null)}
可以看出先依次执行了5-9,再依次执行了0-4。并且在设置了依赖关系后,队列的线程调度也会自动变化。保证了程序性能。
NSOperation与GCD比较
NSOperation是CGD的封装,相比于GCD而言,NSOperation可以更加方便的控制任务,设置依赖,执行,暂停,取消,控制优先级等。NSOperation是对线程的高度抽象,在项目中使用它,会使项目的程序结构更好,子类化NSOperation的设计思路,是具有面向对象的优点(复用、封装),使得实现是多线程支持,而接口简单。
GCD以block为单位,代码简洁。同时GCD中的队列、组、信号量、source、barriers都是组成并行编程的基本原语。对于一次性的计算,或是仅仅为了加快现有方法的运行速度,选择轻量化的GCD就更加方便。
小结
通过NSOperation与NSOperationQueue的结合使用,我们可以实现与GCD相同的多线程并发操作,并且能够更方便的对异步任务进行管理控制。
相关文章
还在用GCD?来看看 NSOperation 吧
NSOperation v.s GCD
iOS多线程编程总结
iOS多线程(三):NSOperationQueue 的使用