【iOS】多线程NSOperation,NSOperationQueue

发布于:2025-06-10 ⋅ 阅读:(17) ⋅ 点赞:(0)

iOS NSOperation 和 NSOperationQueue

前言

前段时间学习了有关于GCD的内容,这段时间对于NSOperation的内容再进行一个学习和总结

NSOperation简介

实际上 NSOperationNSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。

NSOperation需要配合NSOperationQueue来实现多线程。

  • 操作的定义:
    • 执行操作的含义,换句话说其实就是在线程中执行的那段代码,从GCD层面来类比就相当于我们的一个block
    • NSOperation中,我们可以采用他的子类来进行一个封装的操作
  • 操作队列:
    • 这里的队列是值操作队列,就是用来存放操作的队列.不同于GCD中的一个调度队列的FIFO的原则.NSOperation对于添加到队列中的操作,首先进入准备就绪的状态,然后进入就绪状态的操作的开始执行顺序有操作之间的优先级来决定.
    • 操作队列通过设置最大并发操作数来控制并发.串行
    • NSOperationQueue为我们提供了两种不同类型的队列:主队列和自定义队列.主队列运行在主线程上,自定义队列在后台运行

使用步骤

这里我们使用NSOperation的步骤主要分成三步

  • 创建操作:先将需要执行的操作粉装到一个NSOperation对象中
  • 创建队列:创建一个NSOperationQueue对象
  • 将操作加入队列中:将NSOperation对象添加到NSOperationQueue
基本使用:
创建NSOperation
  1. 使用子类NSInvocationOperation
  2. 使用子类NSBlockOperation
  3. 自定义继承于NSOperation的子类,通过实现内部对应的方法来实现封装操作

这里我们先采用第一种方式来处理:

- (void)useInvocationOperation {
    NSInvocationOperation* operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil]; // 创建NSInvocationOperation这个对象
    [operation start];
}
- (void)task1 {
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
        NSLog(@"%ld -----%@", i, [NSThread currentThread]); // 打印对应任务和线程的信息
    }
}

image-20250605200651986

  • 结合上面的图片我们可以看出在主线程中单独使用NSInvocationOperation的话,操作是在当前线程执行的,并没有开启新线程
  • 如果在其他线程中运行就会打印对应的一个线程的信息:[NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];

image-20250605201617788

这里并不可以开启一个新线程:

  • 现在采用第二种方式NSBlockOperation
- (void)useBlockOperation {
    NSBlockOperation* op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%ld----%@", i, [NSThread currentThread]);
        }
    }];
    [op start];
}

image-20250605202307074

  • 和上面一样在主线程调用之后发现它也没有一个开启新线程的能力,如果仅仅采用我们的NSOperation就没有开启新线程

这里我们如果我们采用下面这个方法addExecutionBlock:的话,可以在不同的线程中执行,实现一个并发执行:

- (void)useBlockOperation {
    NSBlockOperation* op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"0----%@", [NSThread currentThread]);
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1----%@",  [NSThread currentThread]);
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2----%@", [NSThread currentThread]);
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3----%@", [NSThread currentThread]);
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"4----%@", [NSThread currentThread]);
        }
    }];
    [op start];
}

image-20250605203237977

  • 这里可以看出使用子类NSBlockOperation并且调用方法AddExecutionBlock的情况下,blockOperaitonWithBlock:方法中的操作和addExecutionBlock:中的操作是在不同的线程中异步执行的,从而验证了blockOperationWithBlock中的操作也可能会在其他线程中执行

一般情况下,如果一个 NSBlockOperation 对象封装了多个操作。NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。

下面采用第三种方法:自定义NSOperation的子类

#import "NanxunOperation.h"

@implementation NanxunOperation
- (void)main {
    if (!self.isCancelled) {
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@", [NSThread currentThread]);
        }
    }
}
@end

image-20250605204139042

  • 可以看出:在没有使用NSOperationQueue,在主线程单独使用自定义继承NSOperation的子类情况下,是在主线程执行操作,并没有开启新线程
创建NSOperationQueue

NSOperationQueue一共有两种队列:主队列,自定义队列.自定义队列包含了串行,并发等多种功能

  • 主队列:
    • 但凡添加到主队列,都会放到主线程中执行
NSOperationQueue* queue = [NSOperationQueue mainQueue];
  • 自定义队列
    • 添加到这种队列中的操作,就会自动放到子线程中执行
    • 同时包含了:串行,并发等功能
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
操作加入队列
  1. -(void)addOperation:(NSOperation*) op:
    • 先创建操作,在讲创建好的操作加入到创建好的队列中
- (void)addOperationToQueue {
    NSOperationQueue* queue = [[NSOperationQueue alloc] init];
    NSInvocationOperation* op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
    NSInvocationOperation* op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
    NSBlockOperation* op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3----%@", [NSThread currentThread]);
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"4----%@", [NSThread currentThread]);
        }
    }];
    [queue addOperation:op];
    [queue addOperation:op1];
    [queue addOperation:op2];
}

image-20250605205410656

  • 这里可以看出通过NSOperation子类创建操作,并且使用addOperation:将操作加入到操作队列后能够开启新线程,进行一个并发执行
  1. - (void)addOperationWithBlock:(void (^)(void))block;
    • 无需先创建操作,在block中添加操作,直接将包含操作的block加入到队列中
- (void)addOperationWithBlockToQueue {
    NSOperationQueue* queue = [[NSOperationQueue alloc] init];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
}
  • 可以看出:使用addOperationWithBlock:将操作加入到操作队列后能够开启新线程,进行并发执行

image-20250605205830321

NSOperationQueue控制串行执行,并发执行

之前我们说过,NSOperationQueue创建的自定义队列同时具有串行,并发功能,上面我们演示了并发功能,怎么实现一个串行功能

这里有一个关键属性maxConcurrentOperationCount, 加做最大并发操作数,用来控制一个特定队列中有多少操作同时参与并发执行

注意:这里 maxConcurrentOperationCount 控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行

  • 最大并发操作数:maxConcurrentOperationCount
    • 默认情况下为-1,表示不进行一个限制,可进行并发执行
    • 值为1,此时队列为串行队列.只能串行执行
    • 大于1的时候.队列为并发队列,操作可并发执行,即使自己设置一个很大的值,系统也会自动调整为min
- (void)addOperationWithBlockToQueue {
    NSOperationQueue* queue = [[NSOperationQueue alloc] init];
    queue.maxConcurrentOperationCount = 1;
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
}

image-20250605211748049

  • 可以看出最大并发数为1的时候,操作是按顺序串行执行的,并且一个操作完成之后,下一个操作才开始执行
  • 当如果设置最大并发操作为1的时候,操作是并发执行的,可以同时执行两个操作.而开启线程的数量是由系统决定的

image-20250605212004054

NSOperation操作依赖

我们可以通过添加操作依赖,让我们更加方便的控制操作之间的一个先后执行顺序:

  • - (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
  • - (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
  • @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组
- (void)addOperationToQueue {
    NSOperationQueue* queue = [[NSOperationQueue alloc] init];
    NSInvocationOperation* op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
    NSInvocationOperation* op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
    NSBlockOperation* op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3----%@", [NSThread currentThread]);
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"4----%@", [NSThread currentThread]);
        }
    }];
    [op addDependency:op1];
    [queue addOperation:op];
    [queue addOperation:op1];
    [queue addOperation:op2];
}

image-20250605212524248

这里通过添加依赖让结果都是op1先执行,op后执行

NSOpeation优先级

NSOperation给我们提供了一个优先级属性,queuePriority属性适用于同一操作队列中的操作,不使用不同操作队列的操作,

// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

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

  • 什么操作才可以被叫做进入就绪状态的操作

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

  • 这个属性决定了进入准备就绪状态下的操作之间的一个执行顺序
  • 如果一个队列中又有高优先级又有第优先级,并且两个操作都已经准备就绪,那么就先执行高优先级的操作
  • 如果一个队列中既有准备就绪状态的操作,有包含了未准备就绪的操作,但还是先执行准备就绪的操作,优先级不可移易取代依赖关系,如果要控制操作间的启动属性,就要采用依赖关系

线程安全

这里我们采用最经典的一个火车买票问题来对于线程安全进行一个分析:

- (void)initTickerStatueSave {
    NSLog(@"%@", [NSThread currentThread]);
    self.ticketCount = 50;
    self.lock = [[NSLock alloc] init];
    NSOperationQueue* que1 = [[NSOperationQueue alloc] init];
    que1.maxConcurrentOperationCount = 1;
    NSOperationQueue* que2 = [[NSOperationQueue alloc] init];
    que2.maxConcurrentOperationCount = 1;
    __weak typeof(self) weakSelf = self;
    NSBlockOperation* op1 = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf saleTicketSafe];
    }];
    NSBlockOperation* op2 = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf saleTicketSafe];
    }];
    [que1 addOperation:op1];
    [que1 addOperation:op2];
}
- (void)saleTicketSafe {
    while (1) {
        [self.lock lock];
        if (self.ticketCount > 0) {
            self.ticketCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"%ld %@", self.ticketCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        }
        [self.lock unlock];
        if (self.ticketCount <= 0) {
            NSLog(@"所有火车票");
            break;
        }
    }
}

image-20250607151843008

通过上面的加锁就实现了一个线程安全.

小结

笔者学习了有关于NSOperation的一些基本用法,之后还会继续学习,如有缺失会继续补充相关内容