多线程 - NSOperation和NSOperationQueue

NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。NSOperation提供任务的封装,NSOperationQueue顾名思义,执行队列,可以自动实现多核并行计算,自动管理线程的生命周期,如果是并发的情况,其底层也使用线程池模型来管理。

NSOperationQueue

操作列队(Operation Queues)是Cocoa中由NSOperationQueue类实现等同于Dispatch Queue的列队。NSOperationQueue管理着一系列的NSOperation对象。当NSOperation对象被添加到列队当中,列队将持有该线程对象一直到明确取消或者任务执行完成为止。列队中的NSOperation对象将根据对象的优先等级和对象的依赖关系来组织和管理,一个应用可以创建多线程列队并且提交线程到线程列队。

当线程被添加到线程列队之后,不能够直接从线程列队中去除线程。线程将在列队中一直到线程完成任务。线程完成任务并不一定意味着线程执行了的任务结束。一个线程能够被取消,取消一个线程任务将把线程对象去除于线程列队并且通知该对象终止执行的任务。对于当前正在执行的任务,这意味着线程对象工作的代码必须先确认线程的取消状态,然后停止,并标记已经完成。

NSOperationQueue 提供了两种队列:主队列和自定义队列

主队列运行在主线程之上,而自定义队列在后台执行。在两种类型中,这些队列所处理的任务都使用 NSOperation 的子类来表述。我们可以通过设置操作列队最大并发操作数(maxConcurrentOperationCount) 来控制并发、串行,设置为1为串行列队,主对列默认是串行队列,设置为其他值为并发列队。

NSOperationQueue是支持KVC和KVO,我们能够监听以下属性:

添加NSOperation到NSOperationQueue的方式

1、添加一个operation

[queue addOperation:operation];

2、添加一组operation

 [queue addOperations:operations waitUntilFinished: NO];

3、添加一个block形式的operation

 [queue addOperationWithBlock:^{ }];

NSOperation添加到queue之后,通常短时间内就会得到运行。但是如果存在依赖,或者整个queue被暂停等原因,可能需要等待。

注意:NSOperation添加到queue之后,绝对不要再修改NSOperation对象的状态。因为NSOperation对象可能会在任何时候运行,因此改变NSOperation对象的依赖或数据会产生不利的影响。你只能查看NSOperation对象的状态, 比如是否正在运行、等待运行、已经完成等

NSOperation

操作对象(operation object)是NSOperation类的实例,你能够使用它执行应用中你想执行的任务。NSOperation是一个抽象的基类,开发中我们很少使用,一般使用它的子类NSInvocationOpeation和NSBlockOperation或者继承NSOperation自定义子类来执行任务。虽然是基类但是NSOperation实现了很多重要的逻辑来确保执行任务的安全,我们只需要专注实际的任务即可。

NSOperation对象支持以下特性:

1、支持operation之间建立依赖性(dependencies),通过设置依赖性能够保证一个operation执行完成之后再继续其他的operation
2、支持使用kvo监听operation执行状态的改变
3、可以修改operation的执行优先级,这将影响operation的执行顺序
4、执行取消操作
5、可添加完成的代码块,在操作完成后执行

启动一个NSOperation有两种方式:

1、将operation添加到NSOperationQueue
2、手动调用operation的start方法,但是该方法并不能够保证是并发执行。 在strat方法被调用之后NSOperation类的isConcurrent方法将告诉我们operation是同步还是异步执行,默认情况下,该方法返回false,这意味operation是同步执行的。因为调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作。

NSInvocationOperation

- (void)setupNSInvocationOperation {
  NSDictionary *dic = @{@"key": @"value"};
  NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self selector: @selector(task:) object: dic];
  [operation start]; // 当开始执行,会调用我们指定的selector,默契情况下方法在主线程执行
}

- (void)task:(id)data {
  NSLog(@"dic = %@", data);
  NSLog(@"currentThread = %@", [NSThread currentThread]);
  NSLog(@"mainThread = %@", [NSThread mainThread]);
}

运行输出

dic = { key = value; }
currentThread = <NSThread: 0x1c0068c80>{number = 1, name = main}
mainThread = <NSThread: 0x1c0068c80>{number = 1, name = main}

从上面可以看到,执行的方法与我们调用的start方法在同一个线程。下面配合NSOperationQueue进行使用,简单流程如下:

1、创建操作:先将需要执行的操作封装到一个NSOperation对象中。
2、创建队列:创建NSOperationQueue对象。
3、将操作加入到队列中:将NSOperation对象添加到 NSOperationQueue 对象中。

当NSOperation对象添加到 NSOperationQueue 对象之后,系统就会自动将NSOperationQueue中的NSOperation取出来,在对应列队的线程中执行操作。如果是主列队那么就是主线程执行,自定义列队就是新线程执行。那么为什么添加到NSOperationQueue里面的operation一定会是并发执行的呢? 因为NSOperationQueue会为每一个添加到队列里面的operation创建一个线程来运行其start函数, 这样每个start都分布在不同的线程里面来实现operation们的并发执行。

简单将上面的start方法进行替换:

- (void)setupNSInvocationOperation {
    NSLog(@"begin thread = %@", [NSThread currentThread]);
    NSDictionary *dic = @{@"key": @"value"};
    NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self selector: @selector(task:) object: dic];
    // [[NSOperationQueue mainQueue] addOperation:operation];  // 主列队工作
    // [[NSOperationQueue currentQueue] addOperation:operation]; // 当前列队
    NSOperationQueue *queue = [[NSOperationQueue alloc]init]; // 新的线程
    [queue addOperation:operation];
}

- (void)task:(id)data {
   NSLog(@"dic = %@", data);
   NSLog(@"currentThread = %@", [NSThread currentThread]);
}

运行输出

begin thread = <NSThread: 0x1c006cac0>{number = 1, name = main}
dic = { key = value;}
currentThread = <NSThread: 0x1c0464f40>{number = 3, name = (null)}

可以看到,该操作在子线程运行,即开启了一个新的线程来执行任务,上面注释了2行代码,一行是向主操作列队添加operation,这样任务会在主线程执行,另外一个是当前列队,即当前运行的列队。

NSBlockOperation

NSBlockOperation也是NSOperation的子类,封装了block进行操作,管理着一个或者多个block。当我们创建NSBlockOperation对象的时候,已经添加了一个block,我们还可以添加多个block,当开始执行NSBlockOperation对象时,该对象会提交所有的block到默认的并发列队,这时候该对象会等待所有的block任务执行完成,当最后一个block执行完成,该操作对象标记执行完成

OC版本

- (void)setupNSBlockOperation {
   NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
      NSLog(@"currentThread = %@", [NSThread currentThread]);
    }];
   NSLog(@"start");
   [operation start];
   NSLog(@"end");
}

运行输出

start
currentThread = <NSThread: 0x1c4070c80>{number = 1, name = main}
end

可以看出,NSBlockOperation 与 NSInvocationOperation 的结果是一样的,Block 中的操作与start方法在同一个线程执行,并且是同步执行的。

Swift版本

func blockOperation() {
     let operation = BlockOperation {
          print("currentThread = \(Thread.current)")
     }
     print("start")
     operation.start()
     print("end")
}

添加多个block

OC版本

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
     NSLog(@"block1: %@", [NSThread currentThread]);
}];

[operation addExecutionBlock:^{
    NSLog(@"block2: %@", [NSThread currentThread]);
}];

[operation addExecutionBlock:^{
    NSLog(@"block3: %@", [NSThread currentThread]);
}];

[operation addExecutionBlock:^{
    NSLog(@"block4: %@", [NSThread currentThread]);
}];

NSLog(@"start");
[operation start];
NSLog(@"end");

运行输出

start
block1 = <NSThread: 0x60800006fb40>{number = 1, name = main}
block3 = <NSThread: 0x600000269c80>{number = 5, name = (null)}
block4 = <NSThread: 0x60000026aec0>{number = 4, name = (null)}
block2 = <NSThread: 0x60000026af40>{number = 3, name = (null)}
end

Swift版本

func blockOperations() {
     let operation = BlockOperation {
            Thread.sleep(forTimeInterval: 2.0)
            print("block1 = \(Thread.current)")
        }
        operation.addExecutionBlock {
            Thread.sleep(forTimeInterval: 2.0)
            print("block2 = \(Thread.current)")
        }
        operation.addExecutionBlock {
            Thread.sleep(forTimeInterval: 2.0)
            print("block3 = \(Thread.current)")
        }
        operation.addExecutionBlock {
            Thread.sleep(forTimeInterval: 2.0)
            print("block4 = \(Thread.current)")
        }

     print("start")
     operation.start()
     print("end")
}

可以看出,第一个block是主线程执行,其余的block是在子线程执行,即并发执行任务。但是有时候也可能实现不了并发,关键还是调用了start方法后,由系统决定执行情况。

配合OperationQueue使用

func blockOperationsWithQueue() {
     let queue = OperationQueue()

     let operation = BlockOperation {
          print("block1 = \(Thread.current)")
     }
     operation.addExecutionBlock {
         print("block2 = \(Thread.current)")
     }
     operation.addExecutionBlock {
         print("block3 = \(Thread.current)")
     }
     operation.addExecutionBlock {
          print("block4 = \(Thread.current)")
     }
     queue.addOperation(operation)
}

运行输出

block4 = <NSThread: 0x608000264fc0>{number = 5, name = (null)}
block2 = <NSThread: 0x608000265040>{number = 3, name = (null)}
block3 = <NSThread: 0x60000026a880>{number = 4, name = (null)}
block1 = <NSThread: 0x60000026adc0>{number = 6, name = (null)}

可以看到真正实现了并发操作,在不同的线程执行对应的任务

操作依赖性(operation dependency)

依赖是实现operation object之间串行执行的一种方式。一个操作对象(operation object)的执行依赖与另外的操作对象,因此我们可以使用依赖创建简单的一对一的依赖,也可以创建复杂的对象依赖,但是需要注意:不要形成依赖循环,这样会导致死锁。

为了建立操作对象之间的依赖,使用addDependency:方法,该方法将在当前操作对象(方法的调用者)与目标操作对象(参数)之间建立依赖,依赖意味着当前操作对象不能被执行,必须等到目标操作对象执行完成才能执行当前操作对象。当然有添加依赖的方法也有去除依赖的方法,我们可以使用removeDependency:去除操作对象之间的依赖。

想象一个场景比如说,对于服务器下载并压缩一张图片的整个过程,你可能会将这个整个过程分为两个操作,显然图片需要等到下载完成之后才能被调整尺寸,所以我们定义网络请求操作是压缩图片操作的依赖,如下代码所示:

[resizingOperation addDependency: networkingOperation]; // 网络请求成功再对图片进行缩放
[operationQueue addOperation: networkingOperation];
[operationQueue addOperation: resizingOperation];

OC版本

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

// 创建operation
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation1: %@", [NSThread currentThread]);
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation2: %@", [NSThread currentThread]);
}];

NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation3: %@", [NSThread currentThread]);
}];

// 设置依赖
[operation1 addDependency:operation2];
[operation2 addDependency:operation3];

// 添加到列队
[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];

Swift版本

 func addDependency() {
      let queue = OperationQueue()
      let operation1 = BlockOperation {
          print("operation1: \(Thread.current)")
      }
      let operation2 = BlockOperation {
          print("operation2: \(Thread.current)")
      }
      let operation3 = BlockOperation {
          print("operation3: \(Thread.current)")
      }
      operation1.addDependency(operation2)
      operation2.addDependency(operation3)

      queue.addOperations([operation1, operation2, operation3], waitUntilFinished: false)
 }

运行结果

operation3: <NSThread: 0x604000662880>{number = 3, name = (null)}
operation2: <NSThread: 0x604000663b80>{number = 4, name = (null)}
operation1: <NSThread: 0x604000663b80>{number = 4, name = (null)}

上面先执行第一个operation3,等operation3执行完成返回isFinish为YES,再执行operation2,等operation2执行完成了,最后执行operation1。

依赖关系不局限于相同queue中的NSOperation对象,NSOperation对象会管理自己的依赖, 因此完全可以在不同的queue之间的NSOperation对象创建依赖关系。上面是单列队中operation之间的依赖,下面看一下多列队的情况

NSOperationQueue *queue1 = [[NSOperationQueue alloc]init];
NSOperationQueue *queue2 = [[NSOperationQueue alloc]init];

// 创建operation
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation1");
    NSLog(@"thread: %@", [NSThread currentThread]);
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation2");
    NSLog(@"thread: %@", [NSThread currentThread]);
}];

NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation3");
    NSLog(@"thread: %@", [NSThread currentThread]);
}];

NSBlockOperation *operation4 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation4");
    NSLog(@"thread: %@", [NSThread currentThread]);
}];

// 设置依赖
[operation1 addDependency:operation3];
[operation3 addDependency:operation2];
[operation2 addDependency:operation4];

// 添加到列队
[queue1 addOperation:operation1];
[queue1 addOperation:operation2];
[queue2 addOperation:operation3];
[queue2 addOperation:operation4];

运行输出

operation4
thread: <NSThread: 0x60400027e440>{number = 3, name = (null)}
operation2
thread: <NSThread: 0x60400027de40>{number = 4, name = (null)}
operation3
thread: <NSThread: 0x60400027e440>{number = 3, name = (null)}
operation1
thread: <NSThread: 0x60400027de40>{number = 4, name = (null)}

可以看到同样也是按照事先的依赖顺序执行。

completionBlock

completionBlock属性,每当一个NSOperation执行完毕,它就会调用它的completionBlock属性一次,这提供了一个非常好的方式进行后续操作。比如说,可以在一个网络请求操作的completionBlock来处理操作执行完以后的数据。

OC版本

// 主列队运行
NSOperationQueue *queue = [NSOperationQueue mainQueue];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation");
    NSLog(@"thread: %@", [NSThread currentThread]);
}];
// 任务执行完成后执行
[operation setCompletionBlock:^{
    NSLog(@"operation finished");
}];
[queue addOperation:operation];

Swift版本

let queue = OperationQueue.main
let operation = BlockOperation {
    print("operation: \(Thread.current)")
}
operation.completionBlock = {
    print("operation finished")
}
queue.addOperation(operation)

运行结果

operation
thread: <NSThread: 0x604000069540>{number = 1, name = main}
operation finished

operation的优先级

NSOperation 提供了queuePriority(优先级)属性,queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但是我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。

优先级只能应用于相同queue中的operations。如果应用有多个operation queue,每个queue的优先级等级是互相独立的。因此不同queue中的低优先级操作仍然可能比高优先级操作更早执行。

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
  NSOperationQueuePriorityVeryLow = -8L,
  NSOperationQueuePriorityLow = -4L,
  NSOperationQueuePriorityNormal = 0,
  NSOperationQueuePriorityHigh = 4,
  NSOperationQueuePriorityVeryHigh = 8
};

@property NSOperationQueuePriority queuePriority;

注意:对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。

那么,什么样的操作才是进入就绪状态的操作呢?

当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。

举个例子,现在有4个优先级都是 NSOperationQueuePriorityNormal(默认级别)的操作:op1,op2,op3,op4。其中 op3 依赖于 op2,op2 依赖于 op1,即 op3 -> op2 -> op1。现在将这4个操作添加到队列中并发执行

因为 op1 和 op4 都没有需要依赖的操作,所以在 op1,op4 执行之前,就是处于准备就绪状态的操作。
而 op3 和 op2 都有依赖的操作(op3 依赖于 op2,op2 依赖于 op1),所以 op3 和 op2 都不是准备就绪状态下的操作。

理解了进入就绪状态的操作,那么我们就理解了queuePriority 属性的作用对象。

1、queuePriority 属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且优先级不能取代依赖关系

2、如果一个队列中既包含高优先级操作,又包含低优先级操作,当两个操作都已经准备就绪,那么队列先执行高优先级操作。比如上例中,如果 op1 和 op4 是不同优先级的操作,那么就会先执行优先级高的操作。

3、如果,一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。

取消operation

一旦添加到operation queue,queue就拥有了这个Operation对象并且不能被删除,唯一能做的事情是取消。你可以调用Operation对象的cancel方法取消单个操作,也可以调用operation queue的cancelAllOperations方法取消当前queue中的所有操作。

 // 取消单个操作
[operation cancel];

// 取消queue中所有的操作
[queue cancelAllOperations];

等待operation完成

为了最佳的性能,你应该设计你的应用尽可能地异步操作,让应用在operation正在执行时可以去处理其它事情。如果需要在当前线程中处理operation完成后的结果,可以使用NSOperation的waitUntilFinished方法阻塞当前线程,等待operation完成。

通常我们应该避免编写这样的代码,阻塞当前线程可能是一种简便的解决方案,但是它引入了更多的串行代码,限制了整个应用的并发性,同时也降低了用户体验。绝对不要在应用主线程中等待一个Operation,只能在第二或次要线程中等待。阻塞主线程将导致应用无法响应用户事件,应用也将表现为无响应。

// 会阻塞当前线程,等到某个operation执行完毕
[operation waitUntilFinished];

除了等待单个Operation完成,你也可以同时等待一个queue中的所有操作,使用NSOperationQueue的waitUntilAllOperationsAreFinished方法。注意:在等待一个 queue时,应用的其它线程仍然可以往queue中添加Operation,因此可能会加长线程的等待时间。

// 阻塞当前线程,等待queue的所有操作执行完毕
[queue waitUntilAllOperationsAreFinished];

暂停和继续queue

如果你想临时暂停Operations的执行,可以使用queue的setSuspended:方法暂停queue。不过暂停一个queue不会导致正在执行的operation在任务中途暂停,只是简单地阻止调度新operation执行。你可以在响应用户请求时,暂停一个queue来暂停等待中的任务。稍后根据用户的请求,可以再次调用setSuspended:方法继续queue中operation的执行

// 暂停queue
[queue setSuspended:YES];
 
// 继续queue
[queue setSuspended:NO];

自定义NSOperation

如果NSBlockOperation和NSInvocationOpeation不能满足你应用需求,我们可以自定义NSOperation,但是在子类化NSOperation之前,你需要考虑你是想实现并发操作( concurrent operation)还是非并发操作( nonconcurrent operation),非并发操作比并发操作简单一些。

非并发执行

对于每一个操作对象,至少需要实现下面两个方法:

1:自定义的初始化方法,方便创建对象
2:重写main方法,执行自己的任务

另外可以根据需要添加额外的方法来辅助实现功能。官方例子

@interface MyNonConcurrentOperation : NSOperation
@property id (strong) myData;
-(id)initWithData:(id)data;
@end

@implementation MyNonConcurrentOperation
- (id)initWithData:(id)data {
  if (self = [super init])
      myData = data;
  return self;
}

-(void)main {
  @try {
    // Do some work on myData and report the results.
  }
  @catch(...) {
    // Do not rethrow exceptions.
  }
}
@end

可以看到上面很简单,仅仅是提供了一个参数为data的初始化方法,而你可以在main里面写上你的代码去执行具体的任务。具体demo下载地址

并发执行

operation对象默认是同步操作的,它们执行任务所在的线程就是调用start方法的线程,如果想实现并发可以重写下列方法:

1:start方法(必须实现)

所有的并发操作都需要重写该方法并且使用自定义实现取代默认行为。手动执行一个操作,你可以调用start方法。因此,这个方法的实现是这个操作的始点,也是其他线程或者运行这你这个任务的起点。注意一下,在这里永远不要调用[super start]

2:main方法(可选)

该方法用于实现操作对象(operation object)所执行的具体任务,虽然我们能够在start方法中执行任务,但是在main方法中实现任务将使代码看起来更加简洁,分离初始化和任务代码

3:isExecuting和isFinished(必须)

并发操作需要负责创建执行的环境并告诉执行环境的状态,因此,并发操作必须持有一些状态信息,比如:什么时候开始执行任务,什么时候任务执行完成,它必须通过这些方法告诉外部当前的状态。这种而且这些方法必须是线程安全,当状态发生改变的时候,你必须使用KVO通知监听这些状态的对象

4:isAsynchronous(必须)

为了确定操作是并发操作,需要重写该方法返回YES。现在是isAsynchronous,isConcurrent被废弃了

官方例子

@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) isAsynchronous {
  return YES;
}

- (BOOL)isExecuting {
  return executing;
}

- (BOOL)isFinished {
  return finished;
}
@end

上面的代码简单实现了isFinish、isExecuting、isConcurrent三个方法,isConcurrent只需要返回YES就可以了。isFinish和isExecuting返回当前实例的属性就可以了。

- (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. 如果是已经取消了,需要把Finish设为YES
    [self willChangeValueForKey:@"isFinished"];
    finished = YES;
    [self didChangeValueForKey:@"isFinished"];
    return;
  }

  // If the operation is not canceled, begin executing the task. 如果没有取消,就继续运行代码
  [self willChangeValueForKey:@"isExecuting"];
  [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"];
}

由代码可知,在并发操作中,我们的start方法将负责以异步方式开启操作(operation),一旦operation启动之后,我们需要在start方法中更新operation的执行状态,使用KVO通知告诉感兴趣的客户,当前operation的状态。如上面代码所示告知当前operation的状态是完成还是正在执行。

自定义的实现要发出合适的KVO通知,因为如果你的NSOperation实现需要用到工作依赖从属特性,而你的实现里没有发出合适的“isFinished”KVO通知,依赖你的NSOperation就无法正常执行。NSOperation是支持KVO操作的,可以监听下列key paths:

isCancelled:判断操作是否已经标记为取消
isConcurrent(Swift中是isAsynchronous):是否是异步执行
isExecuting:返回YES表示操作正在执行,反之则没在执行
isFinished:返回YES表示操作执行成功或者被取消了,该属性非常重要,因为一个操作对象并不会q清除依赖关系一直到isFinished标志为YES
            相似的,操作列队(operation queue)也不会去除操作,一直到isFinished标志为YES
isReady:返回 YES 表示操作已经准备好被执行, 如果返回NO则说明还有其他没有先前的相关步骤没有完成
dependencies:在当前操作开始执行之前完成执行的所有操作对象数组。
queuePriority:列队优先级
completionBlock:完成回调block

如果我们重写了start方法或者执行自定义NSOperation操作,我们必须保证自定义的对象支持KVO监听key paths。如果不了解键值编码和键值监听,可以看KVCKVO

另外还需要注意:当我们自定义NSOperation,我们需要确保被重写的方法在多线程的调用下是安全的。因为NSOperation类是多核的,因此在多线程中调用NSOperation的方法是安全,不需要添加额外的锁来同步访问数据。所以对于自定义NSOperation,我们同样必须确保重写方法的调用线程安全,比如:数据访问,我们需要进行同步来防止潜在的问题。对于同步有兴趣的可以看线程编程

响应取消事件

一旦我们添加operation到operationQueue中之后,我们将不负责operation,操作列队将负责和控制任务的执行。在operation开始执行之后,它将继续执行任务一直到完成任务或者代码中显示的取消此次operation,取消操作能够发生在如何时候,甚至是operation执行之前,为了执行取消操作,需要提供isCancelled方法,该方法非常轻量,能够被频繁调用并且不会存在性能问题,当你在自定义operation的时候,你应该考虑在下列地方使用isCancelled方法:

1、在你执行任意的实际任务之前
2、在循环迭代开始之前至少执行一次,或者是更加频繁的执行
3、在代码中能容易终止operation的地方使用

NSOperation有三种状态,isReady -> isExecuting -> isFinish, 如果在Ready的状态中对NSOperation进行取消,NSOperation会进入Finish状态。但是如果Operation已经开始执行了,就会一直运行到结束,或者由我们进行显示取消。也就是说Operation已经在executing状态,我们调用cancle方法系统不会中止线程的,这需要我们在任务过程中检测取消事件,并中止线程的执行,还要注意一点我们要释放内存或资源

NSOperationQueue控制串行、并发执行

NSOperationQueue实现串行、并发执行任务,需要依靠一个属性maxConcurrentOperationCount,叫做最大并发操作数。用来控制一个特定队列中可以有多少个操作同时参与并发执行。

maxConcurrentOperationCount: 默认情况下为-1,表示不进行限制,可进行并发执行。
maxConcurrentOperationCount: 为1时,队列为串行队列。只能串行执行。
maxConcurrentOperationCount: 大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,
                            即使设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。

例子

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

 queue.maxConcurrentOperationCount = 1; // 串行列队
// queue.maxConcurrentOperationCount = 2; // 并发列队
// queue.maxConcurrentOperationCount = 4; // 并发列队
// queue.maxConcurrentOperationCount = 8; // 并发列队

[queue addOperationWithBlock:^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"1:%@", [NSThread currentThread]);
}];

[queue addOperationWithBlock:^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"2:%@", [NSThread currentThread]);
}];

[queue addOperationWithBlock:^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"3:%@", [NSThread currentThread]);
}];

[queue addOperationWithBlock:^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"4:%@", [NSThread currentThread]);
}];

[queue addOperationWithBlock:^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"5:%@", [NSThread currentThread]);
}];

[queue addOperationWithBlock:^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"6:%@", [NSThread currentThread]);
}];

[queue addOperationWithBlock:^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"7:%@", [NSThread currentThread]);
}];

[queue addOperationWithBlock:^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"8:%@", [NSThread currentThread]);
}];

并发数为1运行输出,按顺序串行输出,一个操作完成之后,下一个操作才开始执行

 1:<NSThread: 0x600000469c00>{number = 3, name = (null)}
 2:<NSThread: 0x600000469b80>{number = 4, name = (null)}
 3:<NSThread: 0x600000469b80>{number = 4, name = (null)}
 4:<NSThread: 0x600000469c00>{number = 3, name = (null)}
 5:<NSThread: 0x600000469b80>{number = 4, name = (null)}
 6:<NSThread: 0x600000469b80>{number = 4, name = (null)}
 7:<NSThread: 0x600000469c00>{number = 3, name = (null)}
 8:<NSThread: 0x600000469b80>{number = 4, name = (null)}

最大并发数为2时,并发执行,运行输出如下,而开启线程数量是由系统决定的,不需要我们来管理

2:<NSThread: 0x60400027be00>{number = 4, name = (null)}
1:<NSThread: 0x600000463c40>{number = 3, name = (null)}
4:<NSThread: 0x600000477500>{number = 5, name = (null)}
3:<NSThread: 0x60000046e480>{number = 6, name = (null)}
5:<NSThread: 0x600000463c40>{number = 3, name = (null)}
6:<NSThread: 0x60400027be00>{number = 4, name = (null)}
7:<NSThread: 0x60000046e480>{number = 6, name = (null)}
8:<NSThread: 0x600000477500>{number = 5, name = (null)}

最大并发数为4时,并发执行,运行输出如下

2:<NSThread: 0x604000463b40>{number = 4, name = (null)}
4:<NSThread: 0x60400046c140>{number = 6, name = (null)}
1:<NSThread: 0x600000460700>{number = 3, name = (null)}
3:<NSThread: 0x6000004602c0>{number = 5, name = (null)}
7:<NSThread: 0x60400046c140>{number = 6, name = (null)}
6:<NSThread: 0x600000460700>{number = 3, name = (null)}
5:<NSThread: 0x604000472040>{number = 7, name = (null)}
8:<NSThread: 0x6000004602c0>{number = 5, name = (null)}

NSOperation线程间的通信

在 iOS 开发过程中,我们一般都在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。当在其他线程完成了耗时操作时,需要回到主线程,那么就用需要用到了线程之间的通讯。

NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperationWithBlock:^{
    [NSThread sleepForTimeInterval:2.0]; // 模拟耗时
    NSLog(@"%@", [NSThread currentThread]);
    // 回到主线程
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // UI操作
        NSLog(@"%@", [NSThread currentThread]);
    }];
}];

运行输出

<NSThread: 0x60400026e300>{number = 3, name = (null)}
<NSThread: 0x604000073700>{number = 1, name = main}

NSOperation线程同步和线程安全

线程安全

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步

可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。好比如两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。

例子

模拟火车票售卖的方式,实现 NSOperation 线程安全和解决线程同步问题。 场景:总共有10张火车票,有两个售卖火车票的窗口,一个是深圳火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。

-(void)setupTicketStatus {
    self.ticketsCount = 10;
    self.lock = [[NSLock alloc]init];

    // 创建 queue1,queue1 代表深圳火车票售卖窗口
    NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
    queue1.maxConcurrentOperationCount = 1;

    // 创建 queue2,queue2 代表上海火车票售卖窗口
    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    queue2.maxConcurrentOperationCount = 1;

    // 创建卖票操作operation1
    __weak typeof(self) weakSelf = self;
    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf saleTicketNotSafe];
    }];

    // 创建卖票操作operation2
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
      [weakSelf saleTicketNotSafe];
     
    }];

    // 添加操作,开始卖票
    [queue1 addOperation:operation1];
    [queue2 addOperation:operation2];
}

非线程安全

- (void)saleTicketNotSafe {
    while (1) {
    if (self.ticketsCount > 0) {
        //如果还有票,继续售卖
        self.ticketsCount--;
        NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketsCount, [NSThread currentThread]]);
        [NSThread sleepForTimeInterval:0.2];
     } else {
        NSLog(@"所有火车票均已售完");
        break;
    }
  }
}

运行输出

剩余票数:9 窗口:<NSThread: 0x600000663b40>{number = 3, name = (null)}
剩余票数:8 窗口:<NSThread: 0x604000275580>{number = 4, name = (null)}
剩余票数:7 窗口:<NSThread: 0x600000663b40>{number = 3, name = (null)}
剩余票数:6 窗口:<NSThread: 0x604000275580>{number = 4, name = (null)}
剩余票数:5 窗口:<NSThread: 0x600000663b40>{number = 3, name = (null)}
剩余票数:5 窗口:<NSThread: 0x604000275580>{number = 4, name = (null)}
剩余票数:4 窗口:<NSThread: 0x600000663b40>{number = 3, name = (null)}
剩余票数:3 窗口:<NSThread: 0x604000275580>{number = 4, name = (null)}
剩余票数:1 窗口:<NSThread: 0x604000275580>{number = 4, name = (null)}
剩余票数:2 窗口:<NSThread: 0x600000663b40>{number = 3, name = (null)}
剩余票数:0 窗口:<NSThread: 0x600000663b40>{number = 3, name = (null)}
剩余票数:0 窗口:<NSThread: 0x604000275580>{number = 4, name = (null)}
所有火车票均已售完
所有火车票均已售完

可以看到:在不考虑线程安全的情况下,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题。

线程安全

通过对线程进行加锁保证线程安全执行,将前面block中的saleTicketNotSafe方法替换为saleTicketSafe

- (void)saleTicketSafe {
    while (1) {
    // 加锁
    [self.lock lock];
    if (self.ticketsCount > 0) {
        //如果还有票,继续售卖
        self.ticketsCount--;
        NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketsCount, [NSThread currentThread]]);
        [NSThread sleepForTimeInterval:0.2];
    }
    // 解锁
    [self.lock unlock];
    if (self.ticketsCount <= 0) {
        NSLog(@"所有火车票均已售完");
        break;
    }
  }
}

运行输出

剩余票数:9 窗口:<NSThread: 0x60400046d780>{number = 3, name = (null)}
剩余票数:8 窗口:<NSThread: 0x60000027fbc0>{number = 4, name = (null)}
剩余票数:7 窗口:<NSThread: 0x60400046d780>{number = 3, name = (null)}
剩余票数:6 窗口:<NSThread: 0x60000027fbc0>{number = 4, name = (null)}
剩余票数:5 窗口:<NSThread: 0x60400046d780>{number = 3, name = (null)}
剩余票数:4 窗口:<NSThread: 0x60000027fbc0>{number = 4, name = (null)}
剩余票数:3 窗口:<NSThread: 0x60400046d780>{number = 3, name = (null)}
剩余票数:2 窗口:<NSThread: 0x60000027fbc0>{number = 4, name = (null)}
剩余票数:1 窗口:<NSThread: 0x60400046d780>{number = 3, name = (null)}
剩余票数:0 窗口:<NSThread: 0x60000027fbc0>{number = 4, name = (null)}
所有火车票均已售完
所有火车票均已售完

可以看出:在考虑了线程安全,使用 NSLock 加锁、解锁机制的情况下,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题。

GCD与NSOperationQueue

首先:GCD 是纯 C 的 API,而操作队列则是 Objective-C 的对象。在 GCD 中,任务用块来表示,而块是个轻量级数据结构。与之相反,“操作”(operation)则是个更为重量级的 Objective-C 对象。虽说如此,但 GCD 并不总是最佳方案。有时候采用对象所带来的开销微乎其微,使用完整对象所带来的好处反而大大超过其缺点。

另外GCD是苹果推荐使用的技术,对于一些简单的操作,比如:只需要执行一次的代码来说,使用 GCD 的 dispatch_once 最为方便,又或者是线程间的通信,异步执行任务然后再回到主线程刷新UI,代码实现非常简单,再或者创建定时器等,简单任务使用GCD非常好。

但是如果想对任务进行更多的控制,那么使用 NSOperation和 NSOperationQueue 的好处多多,前面都已经提到了:

1、取消某个操作。

如果使用操作队列,那么想要取消操作是很容易的。运行任务之前,可以在 NSOperation 对象上调用 cancel 方法,该方法会设置对象内的标志位,用以表明此任务不需要执行,不过,已经启动的任务无法取消。若是不使用操作队列,而是把块安排到 GCD 队列中,那就无法取消了。

2、指定操作间的依赖关系

3、通过键值观测机制监控 NSOperation 对象的属性

NSOperation 对象有许多属性都适合通过键值观测机制(简称KVO)来监听,比如可以通过 isCancelled 属性来判断任务是否已取消,又比如可以通过 isFinished 属性来判断任务是否已完成。如果想在某个任务变更其状态时得到通知,或是想用比 GCD 更为精细的方式来控制所要执行的任务,那么键值观测机制会很有用。

4、指定操作的优先级

操作的优先级表示此操作与队列中其他操作之间的优先关系。优先级高的操作先执行,优先级低的后执行。GCD 则没有直接实现此功能的办法。GCD 的队列确实有优先级,不过那是针对整个队列来说的,而不是针对每个块来说的。而令开发者在 GCD 之上自己来编写调度算法,又不太合适,因此,在优先级这一点上,操作队列所提供的功能要比 GCD 更为便利。NSOperation 对象也有 “线程优先级”(thread priority),这决定了运行此操作的线程处在何种优先级上。用 GCD 也可以实现此功能,然而采用操作队列更为简单,只需设置一个属性。

5、重用 NSOperation 对象

系统内置了一些 NSOperation 的子类(比如 NSBlockOperation)供开发者调用,要是不想用这些子类的话,可以自己来创建了。这些类就是普通的 Objective-C 对象,能够存放任何信息。对象在执行时可以充分利用存在于其中的信息,而且还可以随意调用定义在类中的方法。这就比派发队列中那些简单的块要强大许多。这些 NSOperation 类可以在代码中多次使用,它们符合软件开发中的 “不重复”(DRY)原则。

参考

Concurrency Programming Guide
NSOperation
OperationQueue
NSOperation、NSOperationQueue详尽总结

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

推荐阅读更多精彩内容