• 并发编程之Operation Queue


    随着移动设备的更新换代,移动设备的性能也不断提高,现在流行的CPU已经进入双核、甚至 四核时代。如何充分发挥这些CPU的性能,会变得越来越重要。在iOS中如果想要充分利用多核心CPU的优势,就要采用并发编程,提高CPU的利用率。 iOS中并发编程中主要有2种方式Operation Queue和GCD(Grand Central Dispatch)。下面就来先来说一下Operation Queue。

    异步调用和并发

    在深入之前,首先说说异步调用和并发。这两个概念在并发编程中很容易弄混淆。异步调用是指调用时无需等待结果返回的调用,异步调用往往会触发后台线 程处理,比如NSURLConnection的异步网络回调。并发是指多个任务(线程)同时执行。在异步调用的实现中往往采用并发机制,然而并不是所有异 步都是并发机制,也有可能是其他机制,比如一些依靠中断进行的操作。

    为什么Operation Queue

    Operation Queue提供一个面向对象的并发编程接口,支持并发数,线程优先级,任务优先级,任务依赖关系等多种配置,可以方便满足各种复杂的多任务处理场景。

    • 面向对象接口
    • 支持并发数配置
    • 任务优先级调度
    • 任务依赖关系
    • 线程优先级配置

    NSOperation简介

    iOS并发编程中,把每个并发任务定义为一个Operation,对应的类名是NSOperation。NSOperation是一个抽象类,无法 直接使用,它只定义了Operation的一些基本方法。我们需要创建一个继承于它的子类或者使用系统预定义的子类。目前系统预定义了两个子 类:NSInvocationOperation和NSBlockOperation。

    NSInvocationOperation

    NSInvoationOperation是一个基于对象和selector的Operation,使用这个你只需要指定对象以及任务的selector,如果必要,你还可以设定传递的对象参数。

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

    同时当这个Operation完成后,你还可以获取Operation中Invation执行后返回的结果对象。

    id result = [invacationOperation result];
    

    NSBlockOperation

    在一个Block中执行一个任务,这时我们就需要用到NSBlockOperation。可以通过blockOperationWithBlock:方法来方便地创建一个NSBlockOperation:

    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        //Do something here.
    }];
    

    运行一个Operation

    调用Operation的start方法就可以直接运行一个Operation。

    [operation start];
    

    start方法用来启动一个Operation任务。同时,Operation提供一个main方法,你的所有任务都应该在main中进行处理。默认的start方法中会先做出一些异常判断然后直接调用main方法。如果需要自定义一个NSOperation必须重载main方法来执行你所想要执行的任务。

    @implementation CustomOperation
    
    -(void)main {
       @try {
          // Do some work.
       }
       @catch(...) {
          // Exception handle.
       }
    }
    @end
    

    取消一个Operation

    要取消一个Operation,要向Operation对象发送cancel消息:

    [operation cancel];
    

    当向一个Operation对象发送cancel消息后,并不保证这个Operation对象一定能立刻取消,这取决于你的main中对cancel的处理。如果你在main方法中没有对cancel进行任何处理的话,发送cancel消息是没有任何效果的。为了让Operation响应cancel消息,那么你就要在main方法中一些适当的地方手动的判断isCancelled属性,如果返回YES的话,应释放相关资源并立刻停止继续执行。

    创建可并发的Operation

    由于默认情况下Operation的start方法中直接调用了main方法,而main方法中会有比较耗时的处理任务。如果我们在一段代码连续start了多个Operation,这些Operation都是阻塞地依次执行完,因为第二个Operation必须等到第一个Operation执行完start内的main并返回。Operation默认都是不可并发的(使用了Operation Queue情况下除外,Operation Queue会独自管理自己的线程),因为默认情况下Operation并不额外创建线程。我们可以通过Operation的isConcurrent方法来判断Operation是否是可并发的。如果要让Operation可并发,我们需要让main在独立的线程中执行,并将isConcurrent返回YES。

    @implementation MyOperation{
        BOOL        executing;
        BOOL        finished;
    }
    
    
    - (BOOL)isConcurrent {
        return YES;
    }
    
    - (void)start {
       if ([self isCancelled])
       {
          [self willChangeValueForKey:@"isFinished"];
          finished = YES;
          [self didChangeValueForKey:@"isFinished"];
          return;
       }
    
       [self willChangeValueForKey:@"isExecuting"];
       [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
       executing = YES;
       [self didChangeValueForKey:@"isExecuting"];
    }
    
    - (void)main {
       @try {
            // Do some work.
    
            [self willChangeValueForKey:@"isFinished"];
            [self willChangeValueForKey:@"isExecuting"];
            executing = NO;
            finished = YES;
            [self didChangeValueForKey:@"isExecuting"];
            [self didChangeValueForKey:@"isFinished"];
    
       }
       @catch(...) {
          // Exception handle.
       }
    }
    
    @end
    

    当你自定义了startmain方法时,一定要手动的调用一些KVO通知方法,以便让对象的KVO机制可以正常运作。

    设置Operation的completionBlock

    每个Operation都可以设置一个completionBlock,在Operation执行完成时自动执行这个Block。我们可以在此进行一些完成的处理。completionBlock实现原理是对Operation的isFinnshed字段进行KVO(Key-Value Observing),当监听到isFinnished变成YES时,就执行completionBlock

    operation.completionBlock = ^{
        NSLog(@"finished");
    };
    

    设置Operation的线程优先级

    我们可以为Operation设置一个线程优先级,即threadPriority。那么执行main的时候,线程优先级就会调整到所设置的线程优先级。这个默认值是0.5,我们可以在Operation执行前修改它。

    operation.threadPriority = 0.1;
    

    注意:如果你重载的start方法,那么你需要自己来配置main执行时的线程优先级和threadPriority字段保持一致。

    Operation状态变化

    我们可以通过KVO机制来监听Operation的一下状态改变,比如一个Operation的执行状态或完成状态。这些状态的keypath包括以下几个:

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

    NSOperationQueue

    NSOperationQueue是一个Operation执行队列,你可以将任何你想要执行的Operation添加到Operation Queue中,以在队列中执行。同时Operation和Operation Queue提供了很多可配置选项。Operation Queue的实现中,创建了一个或多个可管理的线程,为队列中的Operation提供可高度自定的执行环境。

    Operation的依赖关系

    有时候我们对任务的执行顺序有要求,一个任务必须在另一个任务执行之前完成,这就需要用到Operation的依赖(Dependency)属性。 我们可以为每个Operation设定一些依赖的另外一些Operation,那么如果依赖的Operation没有全部执行完毕,这个 Operation就不会被执行。

    [operation addDependency:anotherOperation];
    [operation removeDependency:anotherOperation];
    

    如果将这些Operation和它所依赖的Operation加如队列中,那么Operation只有在它依赖的Operation都执行完毕后才可以被执行。这样我们就可以方便的控制Operation执行顺序。

    Operation在队列中执行的优先级

    Operation在队列中默认是按FIFO(First In First Out)顺序执行的。同时我们可以为单个的Operation设置一个执行的优先级,打乱这个顺序。当Queue有空闲资源执行新的Operation 时,会优先执行当前队列中优先级最高的待执行Operation。

    最大并发Operation数目

    在一个Operation Queue中是可以同时执行多个Operation的,Operation Queue会动态的创建多个线程来完成相应Operation。具体的线程数是由Operation Queue来优化配置的,这一般取决与系统CPU的性能,比如CPU的核心数,和CPU的负载。但我们还是可以设置一个最大并发数的,那么 Operation Queue就不会创建超过最大并发数量的线程。

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.maxConcurrentOperationCount = 1;
    

    如果我们将maxConcurrentOperationCount设置为1,那么在队列中每次只能执行一个任务。这就是一个串行的执行队列了。

    Simple Code

    下面我写了一个简单的Simple Code来说明一下Operation和Operation Queue。

    NSBlockOperation *operation5s = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operation5s begin");
        sleep(5);
        NSLog(@"operation5s end");
    }];
    operation5s.queuePriority = NSOperationQueuePriorityHigh;
    NSBlockOperation *operation1s = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operation1s begin");
        sleep(1);
        NSLog(@"operation1s end");
    }];
    NSBlockOperation *operation2s = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operation2s begin");
        sleep(2);
        NSLog(@"operation2s end");
    }];
    
    operation1s.completionBlock = ^{
        NSLog(@"operation1s finished in completionBlock");
    };
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.maxConcurrentOperationCount = 1;
    [queue addOperation:operation1s];
    [queue addOperation:operation2s];
    [queue addOperation:operation5s];
    [queue waitUntilAllOperationsAreFinished];
    

    运行这段代码,我得到了一下输出结果:

    operation1s begin
    operation1s end
    operation5s begin
    operation1s finished in completionBlock
    operation5s end
    operation2s begin
    operation2s end
    

    为了更好的展示队列优先级效果,我把queue的maxConcurrentOperationCount设置为1,以便任务一个一个的执行。从上面日志可以看出,第一个operation1s执行完毕后,会执行operation5s,而不是operation2s,因为operation5s的queuePriorityNSOperationQueuePriorityHigh。而第一个线程总是会第一个执行。在看看2-4行,我们可以看出operation1s的completionBlock比operation5s晚开始执行,说明它不在operation1s的线程中执行的。正如前面所说,completionBlock是通过KVO监听执行,一般会运行在监听所在线程,而不是Operation执行的线程。

    注意事项

    • 当一个Operation被加入Queue中后,请不要对这个Operation再进行任何修改。因为一旦加入Queue,它随时就有可能会被执行,对它的任何修改都有可能导致它的运行状态不可控制。
    • threadPriority仅仅影响了main执行时的线程优先级,其他的方法包括completionBlock都是以默认的优先级来执行的。如果自定义的话,也要注意在main执行前设置好threadPriority,执行完毕后要还原默认线程优先级。
    • 经测试,Operation的threadPriority字段只有在Operation单独执行时有效,在Operation Queue中是无效的。
    • 第一个加入到Operation Queue中的Operation,无论它的优先级有多么低,总是会第一个执行。
  • 相关阅读:
    taro 填坑之路(一)taro 项目回顾
    Redux遵循的三个原则是什么?
    解释一下 Flux
    MVC框架的主要问题是什么?
    与 ES5 相比,React 的 ES6 语法有何不同?
    你了解 Virtual DOM 吗?解释一下它的工作原理
    DOM 事件有哪些阶段?谈谈对事件代理的理解
    CSS:用Less实现栅格系统
    .NET:国际化和本地化
    自定义工作流 之 模型设计与实现
  • 原文地址:https://www.cnblogs.com/NSNULL/p/4624909.html
Copyright © 2020-2023  润新知