Concurrency Programming Guide 3 Operation Queues

Operation Queues

GCD对比

  1. 稍微慢一点点,因为是NSOperationNSOperationQueue基于GCD的(在Mac OS X v10.6 & iOS 4之后1);

  2. Operation Queue可以实现operation的dependency功能,使之在制定的operation完成之后再运行。而且指定线程可以在不同的operation queue里跑;

  3. 可以重用、取消、暂停operation;

  4. 可以使用KVO,方便监控operation的运行状态;

  5. Operation既可以直接调用start方法独立运行也可以add到operation queue里面跑;

  6. 可以使用setCompletionBlock:设置一个operation执行完之后的动作;

  7. 可以定义operation优先级别setQueuePriority:;

使用Operation

可以使用NSInvocationOperationNSBlockOperation和sub class NSOperation;

1. NSInvocationOperation

只需要传入obj和method selector就可以,适合用来修改已经成型的代码走并发。

NSInvocationOperation *tmpOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(myTaskMethod:) object:obj];

2. NSBlockOperation

使用NSBlockOperation可以直接传入一个或多个Block,加到同一个Operation的所有Blocks将会并发执行。只有在所有Blocks执行完之后这个Operation才会算是执行完了

NSBlockOperation *tmpOp = [NSBlockOperation blockOperationWithBlock:^{
    NSLog('log somthing...');
    //do sth
}]

之后可以使用addExecutionBlock:方法来添加额外的Block

3. NSOperation

因为NSOperation是抽象基类,所以需要写子类来实现。

默认isConcurrent方法返回NO,表示Operation是和calling thread同步运行的。

如果你不想把一个Operation加入到Operation queue,而要手动执行并让它和calling thread异步运行,你需要写额外的代码创建新的线程来执行这个Operation哟。

不管add到Operation queue的Operation是否可以和其他Operation并发执行(可能这个Operation加了依赖于其他Operation完成之后才能运行的dependency),Operation queue最终执行它的时候都会创建一个新的thread来异步运行这个Operation。

Define Custom Operation Object

对于非并行的task,至少需要实现main和一个自定义的初始化方法。这个自定义的初始化方法是用来把这个Operation设定好,好让main方法执行。

一些你可能需要实现的方法:

  • main方法中需要调用的方法
  • 访问Operation内部数据/执行结果的Accessor
  • NSCoding 协议方法

还有,为了防止Operation在执行过程中意外中断,还需要在运行过程中检查当前Operation是否被cancel(使用Operation的isCancelled方法)。如果isCancelled方法返回YES,需要马上做出响应,如保存当前的计算结果和释放创建的资源。

  • 因为Operation有可能在执行之前就被cancel了,所以需要在一开始的代码就判断
  • 在循环头也需要判断
  • 在有可能让Operation aboard出去的地方

一个栗子1:

@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 {

      BOOL isDone = NO;
      while (![self isCancelled] && !isDone) {

          // Do some work on myData and set isDone to YES when finished.

        }

   }
   @catch(...) {
      // Do not rethrow exceptions.
} }
@end 

对于需要并行执行的Operation,你需要重写override一些方法来让Operation queue里面的Operation异步并行地执行,虽然Operation queue本来就是异步运行Operation的。

方法 描述
start (必要)手动运行Operation调用的方法,不能调用其super
main (可选)一般把start之后运行的主要代码写在这里
isExecutingisFinished (必要)标明当前执行状态,同时还需要负责实现KVO
isConcurrent (必要)如果Operation需要动态地改变并发方式,可以重写该方法。这里重写override返回YES即可

栗子2:

@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;
}

- (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"];
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
   executing = YES;
   [self didChangeValueForKey:@"isExecuting"];
}

@end


下面这个栗子手动加入了完成Operation的任务后执行的代码:

- (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"];
  }

关于isFinished函数

该函数涉及到KVO

因为Operation queue默认是非并行执行的,如果你不返回YES,接下来得Operation将无法执行。如果你有其他Operation是设置了基于它的dependency,那么它也无法执行哟。

KVO

在重写以上函数的时候,你需要维护以下函数KVO:

  • isCancelled
  • isConcurrent
  • isExecuting
  • isFinished
  • isReady
  • dependencies
  • queuePriority
  • completionBlock

其中,重写override isReady 还会可以自定义Operation的dependency逻辑。如果你需要保持dependency的基本逻辑,记得调用其super方法。还有就是dependency的KVO key path不需要管,除非你重写了addDependency:removeDependency:方法。

Customizing the Execution Behavior of an Operation Object

以下设置对所有Operation obj有效,不管是你自己写的子类还是NSInvocationOperation、NSBlockOperation。

Configuring Interoperation Dependencies

dependency是用来实现Operation之间的运行依赖关系的,可以是一对一、一对多的依赖。例如说Operation1需要在Operation2和Operation3之后运行。
你可以在把Operation添加到OperationQueue或手动运行Operation之前设置:

        NSBlockOperation *op1,*op2,*op3;
        [op1 addDependency:op2];
        [op1 addDependency:op3];

这里并不要求op1,op2,op3在同一个OperationQueue里面。

注意不要创建循环的dependency。

除非你重写override了isReady方法,否则在op2和op3完成之后,op1的isReady状态就会变成YES。

还有dependency依赖KVO运行,重写相关方法时要注意。

修改Operation的优先级别

不要依靠优先级别来控制Operation之间的dependency,要使用dependency还是得用addDependency:方法。

使用函数:setQueuePriority:

优先级别:

typedef enum : NSInteger {
   NSOperationQueuePriorityVeryLow = -8,
   NSOperationQueuePriorityLow = -4,
   NSOperationQueuePriorityNormal = 0,
   NSOperationQueuePriorityHigh = 4,
   NSOperationQueuePriorityVeryHigh = 8
} NSOperationQueuePriority;

修改所属Operation Queue的优先级别

setThreadPriority:

参数:0.0-1.0 大的优先级别高

这个优先级别只会影响当前Operation所属的Operation Queue,如果你重写了Operation的start方法并另外创建了线程来并发task,这个创建的线程将不受这个参数影响。

设置Completion Block

setCompletionBlock:

Tips

内存管理

详情可以参阅《_Advanced Memory Management Programming Guide _》

Avoid Per-Thread Storage
  • 在Operation obj中避免直接和线程打交道。
  • 创建Operation的时候就应该把task和相关的变量创建好,避免在运行时创建基于线程的变量(以防运行完线程和数据都被释放)。
Keep References to Your Operation Object As Needed

Operation Queue并不负责保存Operation的指针,它只会尽快的执行添加到里面的Operation,运行完之后会remove出去。所以如果你需要在Operation执行完之后取出保存在里面的数据,你需要保存指向Operation的指针避免它被release。

Catch Errors 和 Exceptions

这些情况下,要考虑try-catch:

  • Check and handle UNIX errno-style error codes.
  • Check explicit error codes returned by methods and functions.
  • Catch exceptions thrown by your own code or by other system frameworks.
  • Catch exceptions thrown by the NSOperation class itself, which throws exceptions in the following situations:
    • When the operation is not ready to execute but its start method is called
    • When the operation is executing or finished (possibly because it was canceled) and its start method
      is called again
    • When you try to add a completion block to an operation that is already executing or finished When you try to
    • retrieve the result of an NSInvocationOperation object that was canceled

Determining an Appropriate Scope for Operation Objects

如果有100个task,考虑使用100/4个Operation来执行。

详情参考《Performance Overview

运行Operation

添加到Operation Queue

一个时间里可以并发的OperationQueue有系统负载和可用内核数有关,不是创建越多OperationQueue就可以并发越多Operation。

Operation Queue执行Operation受优先级别和dependency的影响。

不要修改已经添加到Queue的Operation。

获取Operation的运行状态调用isCancelled / isExecuting / isFinished / isReady 等方法。

使用setMaxConcurrentOperationCount:可以设置Operation Queue的并发数量。然而即使设置为1,OperationQueue执行Operation的顺序还是受优先级别和dependency的影响哒哟。这是和GCD的serail dispatch queue不同的。

方法:

addOperations:waitUntilFinished:

addOperationWithBlock:

栗子:

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

[aQueue addOperation:anOp]; // Add a single operation
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations [aQueue addOperationWithBlock:^{
NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];
    /* Do something. */

}];

手动运行Operation

使用start方法运行

start方法会做一些检查工作,如判断是否ready、KVO。

- (BOOL)performOperation:(NSOperation*)anOp
  {
     BOOL        ranIt = NO;
     if ([anOp isReady] && ![anOp isCancelled])
     {
        if (![anOp isConcurrent])
           [anOp start];
        else
           [NSThread detachNewThreadSelector:@selector(start) toTarget:anOp withObject:nil];
        ranIt = YES; 

     }
     else if ([anOp isCancelled])
     {
        // If it was canceled before it was started,
        //  move the operation to the finished state.
        [self willChangeValueForKey:@"isFinished"];
        [self willChangeValueForKey:@"isExecuting"];
        executing = NO;
        finished = YES;
        [self didChangeValueForKey:@"isExecuting"];
        [self didChangeValueForKey:@"isFinished"];
        // Set ranIt to YES to prevent the operation from
        // being passed to this method again in the future.
        ranIt = YES; 
        }
     return ranIt;
  }

Cancel Operations

对Operation

cancel

对Operation Queue

cancelAllOperations

cancel后的Operation也算是finish

等待 Operation 完成

方法:

waitUntilFinished

挂起调用线程,直到Operation执行完。

waitUntilAllOperationsAreFinished

挂起调用线程,直到Queue中所有Operation执行完。

Suspend、Resume Queues

Operation在接收到Suspend消息后还是会执行完当前正在跑的Operation的。只会在Operation之间Suspend。

标签:ios, object-c, operation-queue, nsoperation, ios并发编程, ios多任务多线程