iOS多线程之Operation Queues

iOS中常见的实现多线程并发的有三种方式,NSThread,NSOperation和GCD。Operation Queues实现并发的主要方式是通过NSOperation&NSOperationQueue实现,主要分以下三步,本文的主要结构也大致是如下结构。

  • 实例化NSOperation子类,绑定执行操作
  • 创建NSOperationQueue,将NSOperation实例添加进来
  • 系统自动将NSOperationQueue队列中检测取出和执行NSOperation操作

NSOperation

NSOperation实例我们称之为操作对象,操作对象可以将需要执行的代码和相关数据集成并封装。操作对象通常不直接执行,而是它加入到操作队列中按顺序调用。同时操作对象也可以直接调用start方法来执行任务,但为了能并行执行任务,标准做法是在start内创建线程。
NSOperation本身是抽象基类,因此必须使用它的子类,使用NSOperation子类的方式有2种:

  • 使用系统提供的两个具体子类: NSInvocationOperation和NSBlockOperation
  • 自定义子类继承NSOperation,实现内部相应的方法

NSInvocationOperation

通过object & selector 非常方便地创建一个NSInvocationOperation,我们已经有了一个现成的方法,而方法

NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(run) object:nil];

NSBlockOperation

使用子类NSBlockOperation,通过使用NSBlockOperation来执行一个或多个block,只有当一个NSBlockOperation所关联的所有block都执行完毕时候,这个NSBlockOperation才算完成,有点类似于dispatch_group概念。从上面打印结果看到在多个线程执行任务。addExecutionBlock:可以为NSBlockOperation添加额外的操作。如果当前NSOperation的任务只有一个的话,那肯定不会开辟一个新的线程,只能同步执行。只有NSOperation的任务数>1的时候,这些额外的操作才有可能在其他线程并发执行。

- (void)runInvocationOp{
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
    }];
    for (int i = 0; i < 5; i++) {
        [op addExecutionBlock:^{
            NSLog(@"%d------%@", i,[NSThread currentThread]);
        }];
    }
    
    [op start];
}

执行结果如下

2017-05-10 18:40:04.642 JQMultiThread[19102:1441939] ------<NSThread: 0x60800006d740>{number = 1, name = main}
2017-05-10 18:40:04.643 JQMultiThread[19102:1441939] 1------<NSThread: 0x60800006d740>{number = 1, name = main}
2017-05-10 18:40:04.643 JQMultiThread[19102:1441939] 2------<NSThread: 0x60800006d740>{number = 1, name = main}
2017-05-10 18:40:04.644 JQMultiThread[19102:1441939] 4------<NSThread: 0x60800006d740>{number = 1, name = main}
2017-05-10 18:40:04.644 JQMultiThread[19102:1442083] 3------<NSThread: 0x60800007b6c0>{number = 3, name = (null)}
2017-05-10 18:40:04.644 JQMultiThread[19102:1442071] 0------<NSThread: 0x60800007b700>{number = 4, name = (null)}

自定义NSOperation

通常来说,我们都是通过将operation添加到operation queue来执行operation的,但这并不是必须的,可以通过start方法执行一个operation,但是这种方式是无法保证异步执行的。当系统定义的两个子类NSInvocationOperation和NSBlockOperation不能很好的满足我们的需求时,我们可以自动移自己的NSOperation类,我们可以定义非并发和并发两种不同类型的NSOperation子类,定义非并发要比并发简单得多。
系统预定义的两个子类 NSInvocationOperation 和 NSBlockOperation 不能很好的满足我们的需求时,我们可以自定义自己的 NSOperation 子类,添加我们想要的功能。我们可以自定义非并发和并发两种不同类型的 NSOperation 子类,而自定义一个前者要比后者简单得多。

  • 定义继承自NSOperation的子类,通过实现内部相应的方法来创建任务。
非并发的NSOperation子类

最低限度来说,实现非并发NSOperation子类需要实现两个初始化方法

  • 自定义初始化方法
  • main方法

引用官方文档的例子如下

- (id)initWithURL:(NSURL *)url scanCount:(NSInteger)scanCount
{
    self = [super init];
    if (self)
    {
        self.loadURL = url;
        ourScanCount = scanCount;
    }
    return self;
}

// -------------------------------------------------------------------------------
//  main:
//
//  Examine the given file (from the NSURL "loadURL") to see it its an image file.
//  If an image file examine further and report its file attributes.
//
//  We could use NSFileManager, but to be on the safe side we will use the
//  File Manager APIs to get the file attributes.
// -------------------------------------------------------------------------------

-(void)main {
    if (![self isCancelled])
    {
        // test to see if it's an image file
        if ([self isImageFile:loadURL])
        {
            // in this example, we just get the file's info (mod date, file size) and report it to the table view
            //
            NSNumber *fileSize;
            [self.loadURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:nil];
            
            NSDate *fileCreationDate;
            [self.loadURL getResourceValue:&fileCreationDate forKey:NSURLCreationDateKey error:nil];
            
            NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
            [formatter setTimeStyle:NSDateFormatterNoStyle];
            [formatter setDateStyle:NSDateFormatterShortStyle];
            NSString *modDateStr = [formatter stringFromDate:fileCreationDate];
            
            NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:
                                  [self.loadURL lastPathComponent], kNameKey,
                                  [self.loadURL absoluteString], kPathKey,
                                  modDateStr, kModifiedKey,
                                  [NSString stringWithFormat:@"%ld", [fileSize integerValue]], kSizeKey,
                                  [NSNumber numberWithInteger:ourScanCount], kScanCountKey,  // pass back to check if user cancelled/started a new scan
                                  nil];
            
            if (![self isCancelled])
            {
                // for the purposes of this sample, we're just going to post the information
                // out there and let whoever might be interested receive it (in our case its MyWindowController).
                //
                [[NSNotificationCenter defaultCenter] postNotificationName:kLoadImageDidFinish object:nil userInfo:info];
            }
        }
    }
}
并发的NSOperation子类

在默认情况下,operation 是同步执行的,也就是说在调用它的 start 方法的线程中执行它们的任务。而在 operation 和 operation queue 结合使用时,operation queue 可以为非并发的 operation 提供线程,因此,大部分的 operation 仍然可以异步执行。但是,如果你想要手动地执行一个 operation ,又想这个 operation 能够异步执行的话,你需要做一些额外的配置来让你的 operation 支持并发执行。下面列举了一些你可能需要重写的方法:
start :(Required) ,所有并发执行的 operation 都必须要重写这个方法,替换掉 NSOperation 类中的默认实现。start 方法是一个 operation 的起点,我们可以在这里配置任务执行的线程或者一些其它的执行环境。另外,需要特别注意的是,在我们重写的 start 方法中一定不要调用父类的实现;
main :(Optional) ,通常这个方法就是专门用来实现与该 operation 相关联的任务的。尽管我们可以直接在 start 方法中执行我们的任务,但是用 main 方法来实现我们的任务可以使设置代码和任务代码得到分离,从而使 operation 的结构更清晰;
isExecutingisFinished :(Required) ,并发执行的 operation 需要负责配置它们的执行环境,并且对外报告执行环境的状态。因此,一个并发执行的 operation 必须要维护一些状态信息,用来记录它的任务是否正在执行,是否已经完成执行等。此外,当这两个方法所代表的值发生变化时,我们需要生成相应的 KVO 通知,以便外界能够观察到这些状态的变化;
isConcurrent :(Required) ,这个方法用来标识一个 operation 是否是并发的 operation ,需要重写这个方法并返回 YES 。

@interface MyOperation : NSOperation {
    BOOL        executing;
    BOOL        finished;
}
- (void)completeOperation;
@end
 
@implementation MyOperation
- (id)init {
    self = [super init];
    if (self) {
        executing = NO;
        finished = NO;
    }
    return self;
}
 
- (BOOL)isConcurrent {
    return YES;
}
 
- (BOOL)isExecuting {
    return executing;
}
 
- (BOOL)isFinished {
    return finished;
}
@end
- (void)start {
   // Always check for cancellation before launching the task.
   if ([self isCancelled])
   {
      // Must move the operation to the finished state if it is canceled.
      [self willChangeValueForKey:@"isFinished"];
      finished = YES;
      [self didChangeValueForKey:@"isFinished"];
      return;
   }
 
   // If the operation is not canceled, begin executing the task.
   [self willChangeValueForKey:@"isExecuting"];

   //start方法重写之后,苹果不会再主动执行main方法
   [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
   executing = YES;
   [self didChangeValueForKey:@"isExecuting"];
}
- (void)main {
   @try {
 
       // Do the main work of the operation here.
 
       [self completeOperation];
   }
   @catch(...) {
      // Do not rethrow exceptions.
   }
}
 
- (void)completeOperation {
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
 
    executing = NO;
    finished = YES;
 
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

NSOperation生命周期与状态

NSOperation被创建后有几个生命周期,Pending,Ready,Executing,Finished,Cancel状态,前三个状态都可以直接执行cancel的操作。

operation开始执行之后,会一直执行任务直到完成,或者显式地取消操作。取消可能发生在任何时候,甚至在operation执行之前。尽管NSOperation提供了一个方法,让应用取消一个操作,但是识别出取消事件则是我们自己的事情。若operation直接终止, 可能无法回收所有已分配的内存或资源。因此operation对象需要检测取消事件,并优雅地退出执行。NSOperation对象需要定期地调用isCancelled方法检测操作是否已经被取消,如果返回YES(表示已取消),则立即退出执行。不管是自定义NSOperation子类,还是使用系统提供的两个具体子类,都需要支持取消。isCancelled方法本身非常轻量,可以频繁地调用而不产生大的性能损失
以下地方可能需要调用isCancelled:

  • 在执行任何实际的工作之前
  • 在循环的每次迭代过程中,如果每个迭代相对较长可能需要调用多次
  • 代码中相对比较容易中止操作的任何地方
    需要注意的是,为了让我们自定义的operation能够取消操作,我们需要在代码中定期检查isCancelled方法的返回值,一旦检查到这个方法返回YES,就需要立刻停止执行接下来任务。
    如果是进行特定任务比如数据请求或者数据下载,我们可以采用系统重写cancel的方法取消操作,但可能会出现一种情况,就是在检查的过程中,这个操作完成了,一旦进入finished状态后,就cancel不掉了,因为没有这个路径,直接从finished到cancel。AFNetworking和SDWebImage的cancel操作都是直接重写,然后去取消下载或者请求的操作。

AFNetworking 中AFURLConnectionOperation的cancel实现

- (void)cancel {
    [self.lock lock];
    if (![self isFinished] && ![self isCancelled]) {
        [super cancel];

        if ([self isExecuting]) {
            [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
        }
    }
    [self.lock unlock];
}

SDWebImage中SDWebImageDownloaderOperation的cancel实现

- (void)cancel {
    @synchronized (self) {
        if (self.thread) {
            [self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
        }
        else {
            [self cancelInternal];
        }
    }
}

- (void)cancelInternalAndStop {
    if (self.isFinished) return;
    [self cancelInternal];
    CFRunLoopStop(CFRunLoopGetCurrent());
}

- (void)cancelInternal {
    if (self.isFinished) return;
    [super cancel];
    if (self.cancelBlock) self.cancelBlock();

    if (self.connection) {
        [self.connection cancel];
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });

        // As we cancelled the connection, its callback won't be called and thus won't
        // maintain the isFinished and isExecuting flags.
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }

    [self reset];
}


NSOperation 属性的KVO维护

通过观察以下Key paths,可以非常容易地被观察到NSOperation的状态:

  • isCancelled
  • isConcurrent
  • isExecuting
  • isFinished
  • isReady
  • dependencies
  • queuePriority
  • completionBlock
    如果你重写了start方法,或者对除了main函数外的NSOperation进行了重大定制,你必须确保自定义对象保持符合这些关键路径的KVO,当覆盖start方法的时候,最应该关注的key patshs变化是isExecuting和isFinished,因为这两个key paths最容易受到重写start方法的影响

NSOperationQueue

一个NSOperation对象可以通过调用start方法来执行任务,默认是同步执行的。也可以将NSOperation添加到一个NSOperationQueue(操作队列)中去执行,而且是异步执行的。具体的执行不用我们自己去管理,都由操作系统去处理。NSOperationQueue和GCD中的并发队列、串行队列略有不同的是:NSOperationQueue一共有两种队列:主队列(maxConcurrentOperationCount为1)、其他队列。其中其他队列同时包含了串行、并发功能。

主队列:凡是加到主队列中的operation,都会放到主线程执行。

NSOperationQueue *queue = [NSOperationQueue mainQueue];

其他队列:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

将任务加到队列中去

把任务加入到队列中主要是以下方法
- (void)addOperation:,添加一个 operation 到 operation queue 中。
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished: ,添加一组 operation 到 operation queue 中。
- (void)addOperationWithBlock:,直接添加一个 block 到 operation queue 中,而不用创建一个 NSBlockOperation 对象。

- (void)runAddOperation
{
    NSOperationQueue *_queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *_blockOp = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"block operation");
    }];
    
    NSBlockOperation *_copyOp = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"Copy Operation");
    }];
    
    NSInvocationOperation *_invocationOp =[[NSInvocationOperation alloc] initWithTarget:self
                                                                               selector:@selector(runInvocationOp)
                                                                                 object:nil];
    
    [_queue addOperationWithBlock:^{
        NSLog(@"add block");
    }];
    
    [_queue addOperation:_invocationOp];
    [_queue addOperations:@[_blockOp, _copyOp] waitUntilFinished:NO];
}

- (void)runInvocationOp
{
    NSLog(@"send Log");
}

设置最大并发数(maxConcurrentOperationCount)

NSOperationQueue类是设计用来执行操作的并行执行,但你可以强行把maxConcurrentOperationCount设置成1用来执行串行操作。但这个队列的执行顺序并不跟GCD中的串行队列那样完全遵循FIFO,而是由好几个因素决定,比如operation 的 isReady 状态,优先级等。如果不设置maxConcurrentOperationCount的属性,那么它的默认值就是
NSOperationQueueDefaultMaxConcurrentOperationCount,就是系统可设置最大操作并行数。

设置操作的任务依赖

通过配置依赖关系,我们可以让不同的operation串行之行,一个operation只有在它依赖的所有operation都执行完成之后才能开始执行。配置operation的依赖关系要涉及到NSOperation类中的addDependency方法。注意三点
* 依赖是单向的操作,[A addDependency:B],表示A依赖B,但B并不会依赖A,不然会形成循环依赖
* 依赖也并不局限在同一个queue中
* 一定不能形成循环依赖,否则会形成死锁

Operation的优先级

当加入到一个队列中,执行顺序取决于operation的isReady状态以及它们对应的优先级。isReady状态取决于操作之间的相互依赖。但是操作优先级水平则是完全由operation的优先级属性决定。所有新建的操作都有一个默认的normal优先级,但是你可以通过setQueuePriority方法来人为增加或者降低操作优先级。

Operation Queue vs Grand Central Dispatch(GCD)

苹果的文档中已经对两者的差别和使用场景给出了官方的解释

GCD is a low-level API that gives you the flexibility to structure your code in a variety of different ways. In contrast, NSOperation provides you with a default structure that you can use for your asynchronous code. If you're looking for an existing, well-defined structure that's perfectly tailored for Cocoa applications, use NSOperation. If you're looking to create your own structure that exactly matches your problem space, use GCD.

GCD是基于C语言开发的底层API,可以灵活地以各种不同的方式构建代码。相比之下NSOperation给你提供了可异步处理代码的默认结构。如果你正在寻找一种完美适用于Cocoa应用程序的结构请使用NSOperation。如果你希望创建与您的问题完全匹配的自己的结构,可以使用GCD。即可以归结为两点,GCD更底层,而NSOperation则为我们带来了一些面向对象的封装,也带来了方便的灵活性。我自己对两者的归结点如下:

  • NSOperation: 建立在 GCD 的基础之上的,面向对象的解决方案,当operation需要添加相互依赖,或者取消一个正在执行的operation,暂停或者恢复operation queue时候,NSOperation无疑更加灵活。
  • GCD:基于C层级的API,轻量级,FIFO执行并发的方式,使用GCD时候我们并不关心任务的调度情况,而让系统帮我们自动处理。

Simple and Reliable Threading with NSOperation
Concurrency Programming Guide
iOS 并发编程之 Operation Queues

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

推荐阅读更多精彩内容