每日一问13——多线程之NSOperation与NSOperationQueue

NSOperation

看一下官方的介绍:

The NSOperation class itself is an abstract base class that must be subclassed in order to do any useful work.

NSOperation是一个抽象基类,我们需要使用它的子类来完成工作。从使用上来说NSOperation就是对GCDblock的封装,它表示一个要被执行的任务。
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 的使用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容