iOS 并发编程之Operation Queues

830 阅读13分钟

这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战

iOS并发编程系列

  1. iOS 并发编程之Operation Queues
  2. iOS 并发编程之Dispatch Queues
  3. iOS 并发编程之Dispatch Sources

NSOperation 类本身是一个抽象基类,必须对其进行子类化才能完成任何有用的工作。 尽管是抽象的,但这个类确实提供了大量的基础设施,以最大限度地减少必须在自己的子类中完成的工作量。 此外,Foundation 框架提供了两个具体的子类(NSInvocationOperation,NSBlockOperation),可以将它们与现有代码一起使用。

operation objects支持以下关键特性:

  • 支持在operation objects之间建立基于的依赖关系。这些依赖关系会阻止给定操作运行,直到它所依赖的所有操作都完成运行。
  • 支持使用 KVO 通知监控操作执行状态的变化。
  • 支持优先操作,通过操作执行优先级,影响它们的相对执行顺序。
  • 支持取消语义,允许您在执行操作时停止操作。 Operation旨在提高应用程序的并发级别。Operation也是将应用程序的行为组织和封装成简单离散块的好方法。可以将一个或多个operation对象提交到队列,并让相应的工作在一个或多个单独的线程上异步执行,而不是在应用程序的主线程上运行一些代码。

创建 Operation Objects

NSInvocationOperation

- (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.
}

NSBlockOperation

NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{

      NSLog(@"Beginning operation.\n");

      // Do some work.

   }];
   
   //添加额外操作(该操作是异步的)
   [theOp addExecutionBlock:^{

    }];

自定义Operation Object

@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

注意: 并发与非并发操作 通过以上三种方式创建Operation对象后其操作都是同步执行的,尽管通常通过将Operation对象添加到操作队列来执行操作来实现异步,但这并不是必需的。也可以通过调用其 start 方法手动执行操作对象,但这样做并不能保证该操作与您的其余代码同时运行。 NSOperation 类的 isConcurrent 方法告诉你一个操作相对于调用它的 start 方法的线程是同步运行还是异步运行。默认情况下,该方法返回 NO,表示操作在调用线程中同步运行。

如果要实现并发操作(即相对于调用线程异步运行的操作),则必须编写额外的代码来异步启动操作。例如,您可能会生成一个单独的线程、调用异步系统函数或执行其他任何操作来确保 start 方法启动任务并立即返回,并且很可能在任务完成之前返回。

您将非并发操作提交到操作队列时,队列本身会创建一个线程来运行您的操作。因此,将非并发操作添加到操作队列仍然会导致操作对象代码的异步执行。只有在需要异步执行操作而不将其添加到操作队列的情况下,才需要定义并发操作的能力.

自定义并发操作

@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

//start需要重写,该方法是调用起点,且不能实现super方法
- (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.

     //添加kvo以便监听状态
      [self willChangeValueForKey:@"isFinished"];
      finished = YES;

      [self didChangeValueForKey:@"isFinished"];
      return;

   }
   // If the operation is not canceled, begin executing the task.
  //  该方法还更新执行成员变量并为 isExecuting 键路径生成 KVO 通知,以反映该值的更改
   [self willChangeValueForKey:@"isExecuting"];

   [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];

   executing = YES;

   [self didChangeValueForKey:@"isExecuting"];

}

//该方法只需启动一个新线程并将其配置为调用 main 方法
- (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"];

}

保持 KVO 合规性

NSOperation 类符合以下键路径的键值观察 (KVO):

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

自定义Operation的执行行为

以下配置行为均发生在Operation对象创建之后,加到队列(NSOperationQueen)之前,Operation对象包括继承了 NSOperation的自定义类和子类.

添加依赖

要在两个操作对象之间建立依赖关系,可以使用 NSOperation 的 addDependency: 方法. 依赖关系也不限于同一队列中的操作。Operation Object管理它们自己的依赖关系,因此在操作之间创建依赖关系并将它们全部添加到不同的队列中是完全可以接受的。然而,不能接受的一件事是在操作之间创建循环依赖关系.

更改操作的执行优先级

对于添加到队列中的操作,执行顺序首先取决于排队操作的准备情况,然后是它们的相对优先级。就绪是由一个操作对其他操作的依赖决定的,但优先级是操作对象本身的一个属性。默认情况下,所有新操作对象都具有“正常”优先级,但可以根据需要通过调用对象的 setQueuePriority: 方法来增加或减少该优先级。

优先级仅适用于同一操作队列中的操作。如果您的应用程序有多个操作队列,则每个队列都会独立于任何其他队列对自己的操作进行优先级排序。因此,低优先级操作仍然可以在不同队列中的高优先级操作之前执行。

优先级不能替代依赖关系。优先级决定了操作队列仅开始执行那些当前准备好的操作的顺序。例如,如果队列同时包含高优先级和低优先级操作,并且这两个操作都已就绪,则队列首先执行高优先级操作。但是,如果高优先级的操作没有准备好,而低优先级的操作已经准备好了,那么队列首先执行低优先级的操作。如果要防止一个操作在另一操作完成之前启动,则必须改用依赖.

设置Completion Block

要设置完成block,使用 NSOperation 的 setCompletionBlock: 方法.

使用NSOperation注意事项

内存管理相关

避免按线程存储

尽管大多数操作在线程上执行,但在非并发操作的情况下,该线程通常由操作队列提供。如果一个操作队列提供了一个线程,该线程归队列所有,并且不会被开发者的操作触及。具体来说,开发者永远不应该将任何数据与不是您自己创建或管理的线程相关联。操作队列管理的线程根据系统和应用程序的需要来来去去。因此,使用每线程存储在操作之间传递数据是不可靠的,并且可能会失败

在Operation object中,任何情况下都没有理由使用线程存储。当您初始化一个Operation对象时,应该为该对象提供完成其工作所需的一切。因此,operation对象本身提供了您需要的上下文存储。所有传入和传出的数据都应该存储在那里,直到它可以集成回您的应用程序或不再需要.

根据需要保留对操作对象的引用

Operation对象添加到队列后,队列会分派和执行操作,在许多情况下,队列在添加后几乎立即开始执行操作,执行完相关的Operation对象后会从队列中删除,如果代码逻辑中有对该Operation对象的引用可能会导致错误,所以需要根据需要手动保留对Operation对象的引用.

处理错误和异常

NSOperation 类提供的默认启动方法不会捕获异常,需要开发者操作捕获和抑制异常。还应该检查错误代码并根据需要通知应用程序的适当部分。如果替换了 start 方法,必须同样在你的自定义实现中捕获任何异常,以防止它们离开底层线程的范围。

应该准备处理的错误情况类型如下:

  • 检查和处理 UNIX errno 样式的错误代码。
  • 检查方法和函数返回的显式错误代码。
  • 捕获您自己的代码或其他系统框架抛出的异常。
  • 捕获 NSOperation 类本身抛出的异常,在以下情况下会抛出异常:
  1. 当操作尚未准备好执行但调用了它的 start 方法时
  2. 当操作正在执行或完成时(可能是因为它被取消了)并且它的 start 方法被再次调用
  3. 当您尝试将完成块添加到已执行或已完成的操作时
  4. 当您尝试检索已取消的 NSInvocationOperation 对象的结果时 如果您的自定义代码确实遇到异常或错误,应该采取任何必要的步骤将该错误传播到应用程序的其余部分。 NSOperation 类不提供将错误结果代码或异常传递给应用程序其他部分的显式方法。因此,如果此类信息对您的应用程序很重要,必须提供必要的代码。

确定操作对象的适当范围

尽管可以将任意数量的操作添加到操作队列中,但这样做通常是不切实际的。像任何对象一样, NSOperation 类的实例会消耗内存并具有与其执行相关的实际成本。如果每个操作对象只做少量工作,而创建了数万个,可能会发现您在调度操作上花费的时间比实际工作要多。如果您的应用程序已经受到内存限制,您可能会发现仅在内存中拥有数万个操作对象可能会进一步降低性能。

有效使用Operation的关键是在需要完成的工作量和保持计算机繁忙之间找到适当的平衡。尝试确保Operation完成了合理的工作量。

还应该避免一次将大量操作添加到队列中,或者避免将操作对象连续添加到队列中的速度快于它们的处理速度。与其用操作对象淹没队列,不如批量创建这些对象。当一个批次完成执行时,使用完成块告诉您的应用程序创建一个新批次。当有很多工作要做时,希望保持队列充满足够的操作以使计算机保持忙碌,但您不希望一次创建如此多的操作以致应用程序内存不足。

当然,创建的操作对象的数量以及您在每个对象中执行的工作量是可变的,并且完全取决于您的应用程序。应该始终使用 Instruments 等工具来帮助您在效率和速度之间找到适当的平衡。

使用Operations

Adding Operations to an Operation 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

尽管操作队列是运行操作对象最方便的方式,但也可以在没有队列的情况下执行Operation。使用的 start 方法启动它。

在其 isReady 方法返回 YES 之前,该Operation不能够运行。 isReady 方法集成到 NSOperation 类的依赖管理系统中,以提供Operation依赖的状态。只有当它的依赖关系被清除时,一个Operation才可以开始执行。

手动执行Operation时,使用 start 方法开始执行, start 方法在实际运行自定义代码之前会执行多项安全检查。特别是,默认的启动方法会生成操作正确处理其依赖关系所需的 KVO 通知。如果您的Operation已被取消,此方法还可以正确地避免执行Operation,如果Operation实际上还没有准备好运行,则会引发异常。

如果定义了并发操作对象,还应该考虑在启动它们之前调用操作的 isConcurrent 方法。如果此方法返回 NO,本地代码可以决定是在当前线程中同步执行操作,还是先创建一个单独的线程。但是,实施这种检查完全取决于您。

- (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

一旦添加到Operation Queen中,Operatio对象实际上归队列所有并且不能被移除。 使操作出队的唯一方法是cancel。 可以通过调用其cancel方法来取消单个操作对象,也可以通过调用队列对象的 cancelAllOperations 方法来取消队列中的所有操作对象。

只有在确定不再需要操作时才应取消操作。 发出取消命令会将操作对象置于“已取消”状态,从而阻止其运行。 因为一个被取消的操作仍然被认为是“完成的”,依赖它的对象会收到适当的 KVO 通知来清除这个依赖。 因此,更常见的是取消所有排队的操作以响应一些重要事件,例如应用程序退出或用户特别请求取消,而不是选择性地取消操作.

等待操作完成

为了获得最佳性能,应该将操作设计为尽可能异步,让应用程序在操作执行时可以自由地做额外的工作。 如果创建操作的代码也处理该操作的结果,可以使用 NSOperation 的 waitUntilFinished 方法来阻止该代码,直到操作完成。 不过,一般来说,最好避免调用此方法。 阻塞当前线程可能是一个方便的解决方案,但它确实会在您的代码中引入更多的序列化并限制并发的总量。

除了等待单个操作完成之外,您还可以通过调用 NSOperationQueue 的 waitUntilAllOperationsAreFinished 方法来等待队列中的所有操作。 在等待整个队列完成时,请注意应用程序的其他线程仍然可以向队列添加操作,从而延长等待时间.

重要提示:该操作不能再主线程使用, 应该只从辅助线程或其他操作中执行此操作。程阻塞主线程会阻止您的应用程序响应用户事件,并可能使您的应用程序看起来没有响应。

暂停和恢复队列

如果要暂时停止操作的执行,可以使用 setSuspended: 方法暂停相应的操作队列。 暂停队列不会导致已经执行的操作在其任务中间暂停。 它只是防止新操作被安排执行。 可能会暂停队列以响应用户暂停任何正在进行的工作的请求,因为期望用户最终可能希望恢复该工作。