多线程-NSOperation/NSOperationQueue

650 阅读3分钟

1.先来理解几个概念

NSOperation:字面意思:操作。

操作就是要去“去执行一件事情”这个过程,,这个事情可以有一个任务,也可以有多个任务,比如在NSBlockOperation 可以通过 addExecutionBlock 来追加需要操作的任务。在 iOS中,NSOperation 是一个抽象化的类,我们需要使用 NSOperation 的具体化子类来进行使用,其中包括 NSInvocationOperation、NSBlockOperation,也可以自定义 NSOperation 的子类去使用。

NSOperationQueue: 操作队列。

在 iOS系统中有两种操作队列,一种是系统提供的主操作队列,主队列运行在主线程中,不用开发人员创建,获取方式是通过[NSOperationQueue mainQueue]获取,另一种是我们自己创建的操作队列

操作队列相当于一个存放操作的容器,我们创建容器,然后再将创建好的操作放入这个容器中,然后系统开始执行操作。

2.如何用NSOperation/NSOperationQueue实现多线程编程

NSOperation/NSOperationQueue实现多线程编程总共分为三步。

第一步,创建一个容器,也就是创建操作队列。

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

截屏2021-03-23 下午2.23.14.png

第二步,创建要执行的操作。

截屏2021-03-23 下午2.29.52.png

第三步,将创建好的操作加入到操作队列中。

截屏2021-03-23 下午2.39.34.png

3.NSOperation的使用

单独使用操作时,需要对操作对象调用 star 来时操作开始执行。

3.1 在主线程使用 NSInvocationOperation

- (void)invocationOperationOnMainThread {
    NSInvocationOperation *invacationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doWork) object:nil];
    [invacationOperation start];
}

...

- (void)doWork {
    for (int i = 0; i < 2; i ++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"A ----- %d -----%@",i,[NSThread currentThread]);
    }
}

控制台输出:

截屏2021-03-23 下午3.35.18.png

说明操作是在当前主线程执行的,没有开启新线程去执行操作。

3.2 在分线程使用 NSInvocationOperation

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

···

- (void)invocationOperationOnOtherThread {
    NSLog(@"--- begin ---");
    
    NSInvocationOperation *invacationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doWork) object:nil];
    [invacationOperation start];
    NSLog(@"--- end ---");
}

控制台输出:

截屏2021-03-23 下午3.41.47.png

说明在其他线程中使用子类 NSInvocationOperation 时,操作是在当前调用的其他线程执行的,也并没有开启新线程。

3.3 在主线程使用 NSBlockOperation

在主线程执行一个 NSBlockOperation 时:

- (void)blockOperationOnMainThread {
    NSLog(@"--- begin ---");
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"A ----- %d ----- %@",i,[NSThread currentThread]);
        }
    }];
    [blockOperation start];
    NSLog(@"--- end ---");
}

控制台输出:

截屏2021-03-23 下午3.44.55.png

说明在主线程中单独使用 NSBlockOperation 执行一个操作的情况下,操作是在当前主线程执行的,并没有开启新线程。

如果我在后面追加几个操作呢?

#pragma mark - 主线程使用 NSBlockOperation
- (void)blockOperationOnMainThread {
    NSLog(@"--- begin ---");
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"A ----- %d ----- %@",i,[NSThread currentThread]);
        }
    }];
    
    //如果不添加下面的多个 ExecutionBlock 任务,那么任务A 都是在主线程执行,如果添加了多个任务,那么后面添加的部分任务也是会在分线程中执行的。
    [blockOperation addExecutionBlock:^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"B -----%d ----- %@",i,[NSThread currentThread]);
        }
    }];

    [blockOperation addExecutionBlock:^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"C ----- %d -----%@",i,[NSThread currentThread]);
        }
    }];
    [blockOperation start];
    NSLog(@"--- end ---");
}

控制台输出:

截屏2021-03-23 下午3.47.03.png

说明当我们添加多个NSBlockOperation的话,系统会自动开辟多个线程去执行操作,具体是在主线程还是在分线程执行block中的操作,由系统决定。

3.4 在分线程使用 NSBlockOperation

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

···

#pragma mark - 分线程使用 NSBlockOperation
- (void)blockOperationOnOtherThread {
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"A ----- %d ----- %@",i,[NSThread currentThread]);
        }
    }];
    
    [blockOperation addExecutionBlock:^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"B -----%d ----- %@",i,[NSThread currentThread]);
        }
    }];

    [blockOperation addExecutionBlock:^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"C ----- %d -----%@",i,[NSThread currentThread]);
        }
    }];
    [blockOperation start];
}

控制台输出:

截屏2021-03-23 下午3.51.42.png

说明在分线程执行多个 NSBlockOperation的话,系统会根据多个操作任务自动开启多个线程进行异步操作。

3.5 操作和操作队列配和使用实现多线程异步执行

- (void)creatOperationQueueAddOperation {
    //创建操作队列  OperationQueue
    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    
    //创建操作 Operation 如果只有doWork 任务也是在分线程执行的。
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doWork) object:nil];
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"B ----- %d -----%@",i,[NSThread currentThread]);
        }
    }];
    [blockOperation addExecutionBlock:^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"C ----- %d -----%@",i,[NSThread currentThread]);
        }
    }];

    //可使用 addOperationWithBlock 直接添加操作任务
    [operationQueue addOperationWithBlock:^{
        for (int i = 0; i < 2; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"D ----- %d -----%@",i,[NSThread currentThread]);
        }
    }];
    [operationQueue addOperation:invocationOperation];//addOperation 操作会将 operation自动 star
    [operationQueue addOperation:blockOperation];
}

控制台输出:

截屏2021-03-23 下午4.42.41.png

操作的执行在 begin 和 end 之后,使用 NSOperation 子类创建操作,并使用 addOperation: 将操作加入到操作队列后能够实现操作的并发执行。

addOperation 操作会将 operation自动 star

可使用 addOperationWithBlock 直接往操作队列添加操作任务。addOperationWithBlock 相当于直接添加了一个 NSBlockOperation 操作

3.6 操作间调度/通信

使用场景:开启一个操作队列去执行复杂而且耗时的操作,然后调度到主队列更新UI

#pragma mark - 线程间通信 调度
- (void)operationCommunicate {
    NSOperationQueue *queue =[[NSOperationQueue alloc] init];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"C ---%@", [NSThread currentThread]);
        }
        
        //调度到主线程执行 比如UI刷新
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2];
                NSLog(@"主线程执行任务 %d ---%@", i,[NSThread currentThread]);
            }
        }];
    }];
}

控制台输出:

截屏2021-03-23 下午4.47.19.png

3.7 设置最大并发数控制串行/并发 setMaxConcurrentOperationCount

#pragma mark - 使用 maxConcurrentOperationCount (最大并发数)参数设置串行/并发
- (void)exchangeSerialOrConcurrentQueueWithMaxConcurrentCount {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//    queue.maxConcurrentOperationCount = 0; //不会执行
//    queue.maxConcurrentOperationCount = -1; //默认为-1,直接开启并发队列
//    queue.maxConcurrentOperationCount = 1;//串行队列
//    queue.maxConcurrentOperationCount = 2;//两个并发
//    queue.maxConcurrentOperationCount = 4;//并发队列
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"A---%@", [NSThread currentThread]);
        }
    }];
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"B---%@", [NSThread currentThread]);
        }
    }];
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"C---%@", [NSThread currentThread]);
        }
    }];
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"D---%@", [NSThread currentThread]);
        }
    }];
}

当 queue.maxConcurrentOperationCount = 1 时,控制台输出为:

截屏2021-03-23 下午4.56.12.png

当 queue.maxConcurrentOperationCount = 2 时,控制台输出为:

截屏2021-03-23 下午4.58.08.png

可见设置不同的 maxConcurrentOperationCount,可以实现任务执行的串行和并发控制。

另外**设置最大并发操作的数量并不等于开辟线程的数量。具体开启几条线程去执行是由系统控制的。**

3.8 操作依赖 addDependency

#pragma mark - 操作依赖addDependency
- (void) addOperationDependency {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doWork) object:nil];
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"B---%@", [NSThread currentThread]);
        }
    }];
    [blockOperation addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"B addExecution ---%@", [NSThread currentThread]);
        }
    }];
    
    NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"C ---%@", [NSThread currentThread]);
        }
    }];
    
    //依赖关系
    //invocationOperation --》 blockOperation --》 blockOperation2
    [invocationOperation addDependency:blockOperation];//让invocationOperation 依赖于 blockOperation,则先执行blockOperation,在执行invocationOperation
    [blockOperation addDependency:blockOperation2];
    
    [queue addOperation:invocationOperation];
    [queue addOperation:blockOperation];
    [queue addOperation:blockOperation2];
//    [queue addOperations:@[invocationOperation,blockOperation,blockOperation2] waitUntilFinished:NO];
}

控制台输出:

截屏2021-03-23 下午5.12.54.png

添加依赖关系依赖关系 invocationOperation --》 blockOperation --》 blockOperation2 后,执行顺序为 blockOperation2 --》blockOperation --》 invocationOperation。

3.9 操作优先级 setQueuePriority

我们在 addOperationDependency 方法中,对 invocationOperation 设置优先级 为 NSOperationQueuePriorityHigh,当执行后发现执行顺序没有变化, 说明存在依赖关系的操作任务,并不会因为优先级的改变而改变执行顺序,优先级的设置不能取代打破/依赖关系。

4. 线程安全/数据安全

还是想 GCD 中买票的情景: 对 NSOperationQueue 加锁,可以保证线程安全和数据安全。加锁方式和GCD方式类似,可以使用信号量添加自旋锁,可以使用synchronized添加自旋锁,也可以使用NSLock

#pragma mark - 线程安全数据安全
- (void)initTicketStatusSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    __weak __typeof(self)weakSelf = self;
    self.ticketSurplusCount = 5;
    // queue1 代表北京火车票售卖窗口的操作队列
    NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
    queue1.maxConcurrentOperationCount = 1;
    // queue2 代表上海火车票售卖窗口的操作队列
    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    queue2.maxConcurrentOperationCount = 1;
    
    //创建买票的操作
    NSBlockOperation *shanghaiOperation = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf saleTicketSafe];
    }];
    NSBlockOperation *beijingOperation = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf saleTicketSafe];
    }];
    
    //将操作添加进操作队列
    [queue1 addOperation:shanghaiOperation];
    [queue2 addOperation:beijingOperation];
}

- (void)saleTicketSafe {
    
//    while (1) {
//        // 相当于加锁
//        dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
//        if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
//            self.ticketSurplusCount--;
//            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketSurplusCount, [NSThread currentThread]]);
//            [NSThread sleepForTimeInterval:0.2];
//        } else { //如果已卖完,关闭售票窗口
//            NSLog(@"所有火车票均已售完");
//            dispatch_semaphore_signal(_semaphore);
//            break;
//        }
//        dispatch_semaphore_signal(_semaphore);
//    }
    
        //NSLock
//    while (1) {
//            // 加锁
//            [self.lock lock];
//            if (self.ticketSurplusCount > 0) {
//                //如果还有票,继续售卖
//                self.ticketSurplusCount--;
//                NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
//                [NSThread sleepForTimeInterval:0.2];
//            }
//            // 解锁
//            [self.lock unlock];
//
//            if (self.ticketSurplusCount <= 0) {
//                NSLog(@"所有火车票均已售完");
//                break;
//            }
//        }

    //互斥锁
        while (1) {
            // 相当于加锁
            @synchronized(self) {
                if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
                    self.ticketSurplusCount--;
                    NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketSurplusCount, [NSThread currentThread]]);
                    [NSThread sleepForTimeInterval:0.2];
                } else { //如果已卖完,关闭售票窗口
                    NSLog(@"所有火车票均已售完");
                    break;
                }
            }
        }
}


控制台输出:

截屏2021-03-23 下午5.27.50.png

5.NSOperation/NSOperationQueue 和 GCD 的区别

  1. GCD是底层的C语言构成的API,而NSOperationQueue 是基于GCD封装而成,其相关对象是Objc的对象。在GCD中,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构;而Operation作为一个对象,为我们提供了更多的选择;

  2. NSOperation能够方便地设置依赖关系,我们可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行;

  3. NSOperation 暴露出很多属性和API。 我们可以用 - (void)cancel; 取消操作;

- (BOOL)isFinished 判断操作是否已经结束;

- (BOOL)isCancelled 判断操作是否已经标记为已取消;

- (BOOL)isExecuting 判断操作是否正在在运行;

- (BOOL)isReady 判断操作是否处于准备就绪状态。

所以通过 KVO 的方式,我们可以很方便的知道操作NSOperation 的执行状态,也就是中间态。我们也可以随时取消已经设定要将要准备执行的任务,而GCD没法停止已经加入queue block 中的任务。

注:cancel 方法调用时,已经开始的操作任务就无法阻止了,只能阻止操作队列中下一个准备就绪将要被执行的操作。

  1. 在NSOperation中,我们能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行。

  2. 如果任务之间不太互相依赖,那么我们可以优先使用GCD,如果任务之间有依赖 或者要监听任务的执行情况,那么我们可以优先使用NSOperationQueue。