Operation Queues

393 阅读13分钟

Concurrency Programming Guide

Cocoa operations are an object-oriented way to encapsulate work that you want to perform asynchronously. Operations are designed to be used either in conjunction with an operation queue or by themselves. Because they are Objective-C based, operations are most commonly used in Cocoa-based applications in OS X and iOS.

About Operation Objects

Operation classes of the Foundation framework

  • NSInvocationOperation

    • 包装selector方法
  • NSBlockOperation

    • 并发执行一个或多个block对象
    • 只有当所有的block对象执行完, operation才算结束
  • NSOperation

    • 抽象类, 用于继承自定义Operation

All operation objects support the following key features:

Support for the establishment of graph-based dependencies between operation objects. These dependencies prevent a given operation from running until all of the operations on which it depends have finished running. For information about how to configure dependencies, see Configuring Interoperation Dependencies.

Support for an optional completion block, which is executed after the operation’s main task finishes. (OS X v10.6 and later only.) For information about how to set a completion block, see Setting Up a Completion Block.

Support for monitoring changes to the execution state of your operations using KVO notifications. For information about how to observe KVO notifications, see Key-Value Observing Programming Guide.

Support for prioritizing operations and thereby affecting their relative execution order. For more information, see Changing an Operation’s Execution Priority.

Support for canceling semantics that allow you to halt an operation while it is executing. For information about how to cancel operations, see Canceling Operations. For information about how to support cancellation in your own operations, see Responding to Cancellation Events.

Operations are designed to help you improve the level of concurrency in your application. Operations are also a good way to organize and encapsulate your application’s behavior into simple discrete chunks. Instead of running some bit of code on your application’s main thread, you can submit one or more operation objects to a queue and let the corresponding work be performed asynchronously on one or more separate threads.

特性

  • 支持Operation执行依赖
  • 支持Operation执行结束的block
  • 支持Operation的KVO, 用以监听Operation的执行状态
  • 支持Operation执行的优先级, 从而影响Operation的执行顺序
  • 支持Operation的取消

Operation设计用于提高应用的并发性 将一个或多个Operation提交到Operation Queue, Queue管理线程执行异步任务

Concurrent Versus Non-concurrent Operations

Although you typically execute operations by adding them to an operation queue, doing so is not required. It is also possible to execute an operation object manually by calling its start method, but doing so does not guarantee that the operation runs concurrently with the rest of your code. The isConcurrent method of the NSOperation class tells you whether an operation runs synchronously or asynchronously with respect to the thread in which its start method was called. By default, this method returns NO, which means the operation runs synchronously in the calling thread.

If you want to implement a concurrent operation—that is, one that runs asynchronously with respect to the calling thread—you must write additional code to start the operation asynchronously. For example, you might spawn a separate thread, call an asynchronous system function, or do anything else to ensure that the start method starts the task and returns immediately and, in all likelihood, before the task is finished.

Most developers should never need to implement concurrent operation objects. If you always add your operations to an operation queue, you do not need to implement concurrent operations. When you submit a nonconcurrent operation to an operation queue, the queue itself creates a thread on which to run your operation. Thus, adding a nonconcurrent operation to an operation queue still results in the asynchronous execution of your operation object code. The ability to define concurrent operations is only necessary in cases where you need to execute the operation asynchronously without adding it to an operation queue

  • 通常Operation添加到Operation Queue中就能自行执行并发任务, Operation也支持手动执行, 调用start方法即可, 但是手动调用不保证并发(手动的都是当前线程同步执行, 异步执行需要自定义Operation), isConcurrent方法用来明确当前的Operation执行是同步的还是异步的
  • 如果要是想并发的Operation, 需要额外添加代码, 执行异步操作, 如生成线程, 调用异步函数, 调用start方法后立即返回
  • 通常不需要自己手动实现异步执行的Operation, 提交到Operation Queue就行, Queue会自定创建线程分配给添加进去的Operation, 然后并发执行Operation

Creating an NSInvocationOperation Object

@implementation MyCustomClass
- (NSOperation*)taskWithData:(id)data {
    NSInvocationOperation* theOp = [[NSInvocationOperation alloc] initWithTarget:self
                    selector:@selector(myTaskMethod:) object:data];
 
   return theOp;
}
 
// This is the method that does the actual work of the task.
- (void)myTaskMethod:(id)data {
    // Perform the task.
}
@end

Creating an NSBlockOperation Object

NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
  NSLog(@"Beginning operation.\n");
  // Do some work.
}];

After creating a block operation object, you can add more blocks to it using the addExecutionBlock: method. If you need to execute blocks serially, you must submit them directly to the desired dispatch queue.

  • 可以使用addExecutionBlock:添加多个执行的block到Operation中, 如果要串行执行block任务, 则需要特定的派发队列

Defining a Custom Operation Object

If the block operation and invocation operation objects do not quite meet the needs of your application, you can subclass NSOperation directly and add whatever behavior you need. The NSOperation class provides a general subclassing point for all operation objects. The class also provides a significant amount of infrastructure to handle most of the work needed for dependencies and KVO notifications. However, there may still be times when you need to supplement the existing infrastructure to ensure that your operations behave correctly. The amount of extra work you have to do depends on whether you are implementing a nonconcurrent or a concurrent operation.

Defining a nonconcurrent operation is much simpler than defining a concurrent operation. For a nonconcurrent operation, all you have to do is perform your main task and respond appropriately to cancellation events; the existing class infrastructure does all of the other work for you. For a concurrent operation, you must replace some of the existing infrastructure with your custom code. The following sections show you how to implement both types of object.

  • 自定义Operation需要提供基础功能和KVO通知, 额外的功能添加也取决于实现的Operation是并发的还是非并发的
  • 自定义非并发Operation, 只需要执行主任务, 相应正确的取消事件, 现有的Operation类提供了相应的所有功能
  • 自定义并发的Operation, 需要替换现有的功能和方法, 添加额外的代码

Performing the Main Task

At a minimum, every operation object should implement at least the following methods:

  • A custom initialization method

  • main

You need a custom initialization method to put your operation object into a known state and a custom main method to perform your task. You can implement additional methods as needed, of course, such as the following:

  • Custom methods that you plan to call from the implementation of your main method

  • Accessor methods for setting data values and accessing the results of the operation

  • Methods of the NSCoding protocol to allow you to archive and unarchive the operation object

  • 自定义非并发的Operation至少要实现的方法
    • 自定义的初始化方法, 设置Operation状态
    • main方法, 执行任务
  • 也可额外定义所需要的方法
    • main方法中需要调用的方法
    • 数据访问方法
    • Operation归解档方法

// 至少要实现的方法: 初始化 + 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

NSOperationSample官方示例代码

Responding to Cancellation Events

After an operation begins executing, it continues performing its task until it is finished or until your code explicitly cancels the operation. Cancellation can occur at any time, even before an operation begins executing. Although the NSOperation class provides a way for clients to cancel an operation, recognizing the cancellation event is voluntary by necessity. If an operation were terminated outright, there might not be a way to reclaim resources that had been allocated. As a result, operation objects are expected to check for cancellation events and to exit gracefully when they occur in the middle of the operation.

To support cancellation in an operation object, all you have to do is call the object’s isCancelled method periodically from your custom code and return immediately if it ever returns YES. Supporting cancellation is important regardless of the duration of your operation or whether you subclass NSOperation directly or use one of its concrete subclasses. The isCancelled method itself is very lightweight and can be called frequently without any significant performance penalty. When designing your operation objects, you should consider calling the isCancelled method at the following places in your code:

  • Immediately before you perform any actual work

  • At least once during each iteration of a loop, or more frequently if each iteration is relatively long

  • At any points in your code where it would be relatively easy to abort the operation

your own code should be sure to free up any resources that were allocated by your custom code.

  • 自定义Operation要注意相应取消事件
  • 注意调用isCancelled方法的地方
    • 任务执行之前
    • 任务执行中的迭代,循环
    • 代码中要停止Operation相关的地方
    • 注意资源的释放
- (void)main {
   @try {
      BOOL isDone = NO;
 
      while (![self isCancelled] && !isDone) {
          // Do some work and set isDone to YES when finished
      }
   }
   @catch(...) {
      // Do not rethrow exceptions.
   }
}

Configuring Operations for Concurrent Execution

Operation objects execute in a synchronous manner by default—that is, they perform their task in the thread that calls their start method. Because operation queues provide threads for nonconcurrent operations, though, most operations still run asynchronously. However, if you plan to execute operations manually and still want them to run asynchronously, you must take the appropriate actions to ensure that they do. You do this by defining your operation object as a concurrent operation.

  • Operation调用start是同步执行任务的,在调用start的线程执行, 添加到Operation Queue的Operation是异步执行
  • 手动执行Operation, 并能异步执行, 需要编写额外代码并重写Operation的方法

并发Operation需要复写的方法

  • start(Required)
    • 设置线程,环境
    • 不能调用super方法
  • main(Optional)
    • 任务执行的方法可以写在start中, 也可放在main用于逻辑分离
  • isExecuting``isFinished(Required)
    • 告知外部当前任务执行的状态
    • 要保证其他线程访问时, 线程安全
    • 要实现相应的KVO
  • isConcurrent(Required)
    • 标识当前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"];
   [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"];
}

Maintaining KVO Compliance

NSOperation类支持的建值观察如下

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

If you override the start method or do any significant customization of an NSOperation object other than override main, you must ensure that your custom object remains KVO compliant for these key paths. When overriding the start method, the key paths you should be most concerned with are isExecuting and isFinished. These are the key paths most commonly affected by reimplementing that method.

If you want to implement support for dependencies on something besides other operation objects, you can also override the isReady method and force it to return NO until your custom dependencies were satisfied. (If you implement custom dependencies, be sure to call super from your isReady method if you still support the default dependency management system provided by the NSOperation class.) When the readiness status of your operation object changes, generate KVO notifications for the isReady key path to report those changes. Unless you override the addDependency: or removeDependency: methods, you should not need to worry about generating KVO notifications for the dependencies key path.

Although you could generate KVO notifications for other key paths of NSOperation, it is unlikely you would ever need to do so. If you need to cancel an operation, you can simply call the existing cancel method to do so. Similarly, there should be little need for you to modify the queue priority information in an operation object. Finally, unless your operation is capable of changing its concurrency status dynamically, you do not need to provide KVO notifications for the isConcurrent key path

  • 如果自定义Operation对象,且复写start或是main方法, 要注意维护以上键值, 尤其复写start方法, 需要关注isExecutingisFinished这两个键值(影响start方法的实现)
  • 如果想实现Operation依赖, 需要复写isReady方法, 在满足依赖之前(满足Operation执行条件之前), 都应该强制isReady方法返回NO, 并且生成相应的KVO通知, 如果复写了addDependency: removeDependency:, 那也需要生成dependencies的KVO通知
  • 通常不需要生成KVO通知, 取消Operation对象调用cancel方法即可, 一般情况也不需要改变queue的优先级信息

Customizing the Execution Behavior of an Operation Object

The configuration of operation objects occurs after you have created them but before you add them to a queue

  • 配置Operation对象执行行为应该在创建之后, 添加到queue之前

自定义执行方式

  • 配置依赖
    • 使用addDependency:在两个Operation对象间设置依赖关系
    • Operation对象间的依赖不局限于相同Queue中, 不同Queue中的Operation对象也可设置依赖关系, Operation对象自行管理依赖关系
  • 设置Operation对象在队列中的优先级
    • 对于添加到Operation Queue中的Operation对象执行顺序首先是看Operation对象是否准备好(取决于Operation对象间的依赖关系), 其次再看Operation对象的优先级(Operation自身的属性)
    • 优先级只影响相同Queue中的Operation对象
    • 优先级不能替代依赖关系, 优先级是对已经准备就绪的Operation对象确定执行顺序
    • 使用setQueuePriority:方法设置Operation对象的优先级
    • 确定Operation对象执行顺序, 先看依赖, 再看优先级
  • 改变底层线程优先级
    • OS X v10.6 之后可以配置Operation对象执行的底层线程的优先级
    • 线程管理由kernel管理
    • 高优先级线程有更多的执行机会
    • 设置Operation对象线程优先级可设置在0.0-1.0之间, 默认线程优先级的值为0.5
    • 在将Operation对象添加到Queue中之前, 使用setThreadPriority:方法设置线程优先级
    • 自定义并发Operation, 并复写了start方法, 需要自行管理线程的优先级
  • 配置Completion Block
    • 通常起到一个通知的作用, 告知任务完成
    • 传递的Block一般为一个无参无返回值的

Tips for Implementing Operation Objects

  • Operation对象的内存管理

  • 避免Per-Thread存储

    • Operation执行所需的线程通常由Operation Queue提供和分配
    • Operation对象不应和分配的线程发生交互
    • 不要使用per-thread storage在Operation对象间传递数据, 系统根据需求创建和销毁线程, 所以使用这种方式传递数据是不可靠的, 数据尽量保存在Operation对象中
  • 根据需要保留对Operation对象的使用

    • 加入Operation Queue中的Operation对象总是尽可能尽快的得到执行, 并发执行完之后就会被移除Queue并释放, 如果Operation对象保存的有所需的数据,或是想要查询Operation的执行状态, 则需要保留Operation对象的引用

Handling Errors and Exceptions

  • OS X v10.6及以后, Operation的start方法默认不支持捕获异常(OS X v10.5, 之前是捕获的)
  • 重写start方法,必须捕获异常, 如果可以还需进行相应的异常通知
  • 需要注意并处理的异常有
    • UNIX风格的错误异常
    • 方法和函数的显式错误
    • 自身代码或是系统框架抛出的异常
    • NSOperation对象逻辑错误

NSOperation对象逻辑错误

  • Operation对象还没准备好就调用了start方法
  • Operation对象执行中或者已结束再次调用start方法
  • 向正在执行中或者已经结束的Operation对象添加Operation block
  • 试图获取已经取消了的NSInvocationOperation对象的结果

Determining an Appropriate Scope for Operation Objects

  • 合理平衡使用Operation, 是的Queue合理调度
  • 从执行时间, 消耗内存等考虑(性能问题)

Executing Operations

  • 创建Operation对象, 加入Queue中, Queue会尽快执行Operation的调度
    • 可使用setMaxConcurrentOperationCount:设置Queue的并发量

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

  • 手动执行Operation, 调用start方法
    • 手动执行, 需要Operation对象已经准备好, 需要关注isReady的值, 处理好依赖关系, 发送好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;
}


Canceling Operations

  • 加入到Queue中的Operation对象不能被删除, 只能被取消, 调用Operation的cancel方法即可, 也可调用Queue的cancelAllOperations
  • Operation对象调用cancel后进入canceled状态, 停止执行, 这种状态也视为finished状态, 其他依赖当前的Operation对象的Operation对象会受到KVO通知, 移除依赖关系, 并开始执行

Waiting for Operations to Finish

  • 可使用Operation对象的waitUntilFinished方法阻塞当前线程, 直到Operation对象任务完成后返回
  • 同样的使用NSOperationQueue对象的waitUntilAllOperationsAreFinished方法阻塞当前线程, 直到所有的任务执行完毕后返回
    • 等待中的Queue仍然可以添加Operation对象
  • 注意不要在主线程等待任务执行完毕, 会阻碍用户操作, 降低体验

Suspending and Resuming Queues

  • 使用setSuspended:方法挂起Queue和恢复Queue
  • 设置Queue挂起后, 执行中的Operation任务仍然会继续执行, 只会阻止调度新的Operation任务

理解如有错误 望指正 转载请说明出处