美文网首页
iOS话题:NSOperation、NSOperationQue

iOS话题:NSOperation、NSOperationQue

作者: 勇往直前888 | 来源:发表于2020-05-06 18:17 被阅读0次

    简介

    • NSOperation、NSOperationQueue是对GCD的一层面向对象的封装。

    • 多线程用得多的还是GCD,简单方便。

    • NSOperation、NSOperationQueueGCD不具备的功能,比如查看状态,设置依赖,取消等等。

    • NSOperation不能直接使用,有两个子类NSInvocationOperation、NSBlockOperation。其中NSInvocationOperation把任务作为selector,用得很少,基本不考虑。NSBlockOperation把任务当做block,用得比较多。通过继承NSOperation自定义main方法,在某些特定的场合会用到。

    • NSOperationQueue、NSOperation、block三级的概念来考虑,相对比较方便。

    • 整体上来说NSOperation、NSOperationQueueGCD的封装是比较失败的。相对来说,还是GCD的概念简洁明了。平时,也推荐多用GCD

    • NSOperationQueue只有主队列和自定义队列两种。主队列运行在主线程之上,而自定义队列在后台执行。没有串行和并行队列的概念。有个属性叫最大并发数。maxConcurrentOperationCount这个值为1,效果上就相当于串行队列。====从这个点就可以看出,封装的人脑子有点短路,设计太差,智商不及格。

    • NSOperation如果不加入一个自定义队列NSOperationQueue,那么默认就是在主线程执行,这种状况要避免。

    • 一个NSOperation可以添加多个block,不过最好不要这样做。一个NSOperation只带一个block,概念最简洁。如果业务上确实需要在某个NSOperation上追加操作,addExecutionBlock,可以考虑。===不过从简洁角度考虑,这始终是脱裤子放屁的坏主意。

    简单使用

    场景:将耗时工作放入后台工作线程;完成之后,回主线程更新界面

    // 基本使用
    - (IBAction)baseButtonTouched:(id)sender {
        // 开始之前,修改界面
        self.baseButton.enabled = NO;
        NSLog(@"任务开始前,修改界面...");
        
        // 创建队列,自定义队列默认就是并行的
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        
        // 添加耗时操作;其实是匿名operation,直接加block
        [queue addOperationWithBlock:^{
            NSLog(@"耗时任务开始....");
            NSLog(@"%@",[NSThread currentThread]);
            sleep(5);
            NSLog(@"耗时任务结束");
            
            // 切换到主线程,添加界面更新操作;其实是匿名operation,直接加block
            [NSOperationQueue.mainQueue addOperationWithBlock:^{
                self.baseButton.enabled = YES;
                NSLog(@"更新界面");
                NSLog(@"%@",[NSThread currentThread]);
            }];
        }];
    }
    

    队列NSOperationQueue可以添加匿名的operation,直接加入了具体任务,block

    企业微信截图_8ab5230f-1aa5-44aa-bb15-39e547d2b856.png

    死锁

    • 和循环引用类似,如果存在循环依赖的情况,将会发生死锁。

    • 最简单的情况:A和B两个operation,A依赖B,B依赖,就会发生死锁。

    • 稍微复杂一点的情况:A、B、C三个operation,A依赖B,B依赖C,C依赖A,就会形成死锁。

    • 取消一个operation,比如A,就可以打破依赖循环,破除死锁。operation是可以取消的。

    // 死锁
    - (IBAction)deadLockButtonTouched:(id)sender {
        // 设置操作
        NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"任务A开始....");
            NSLog(@"%@",[NSThread currentThread]);
            sleep(3);
            NSLog(@"任务A结束");
        }];
        
        NSBlockOperation *operationB = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"任务B开始....");
            NSLog(@"%@",[NSThread currentThread]);
            sleep(1);
            NSLog(@"任务B结束");
        }];
        
        NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"任务C开始....");
            NSLog(@"%@",[NSThread currentThread]);
            sleep(5);
            NSLog(@"任务C结束");
        }];
        
        // 添加依赖关系;这里B依赖A,C依赖B,A依赖C,循环依赖,造成死锁
        [operationB addDependency:operationA];
        [operationC addDependency:operationB];
        [operationA addDependency:operationC];
        
        // 将操作加入队列;任务会自动启动
        NSOperationQueue *workQueue = [[NSOperationQueue alloc] init];
        [workQueue addOperations:@[operationA, operationB, operationC] waitUntilFinished:NO];
        
        NSLog(@"调度完毕,主线程空闲。。。。");
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"等了6s都没完成,估计发生死锁了。手动停掉一个");
            [operationA cancel];
        });
    }
    
    企业微信截图_7412d842-766a-48d2-a80a-b9b5aac03f14.png

    多读单写

    • 每个读写操作,都当做一个block,也就是一个任务;

    • 所有写之前的读操作,都集中放入一个readOperationBefore

    • 写操作,单独放入一个writeOperation

    • 所有些之后的读操作,都集中放入一个readOperationAfter

    • 通过依赖关系,保证多读单写的顺序。
      readOperationBefore <= writeOperation <= readOperationAfter

    // 多读单写
    - (IBAction)readWriteButtonTouched:(id)sender {
        // 写之前的读操作
        NSBlockOperation *readOperationBefore = [[NSBlockOperation alloc] init];
        [readOperationBefore addExecutionBlock:^{
            NSLog(@"写之前的读操作1开始....");
            sleep(1);
            NSLog(@"写之前的读操作1结束");
        }];
        [readOperationBefore addExecutionBlock:^{
            NSLog(@"写之前的读操作2开始....");
            sleep(1);
            NSLog(@"写之前的读操作2结束");
        }];
        
        // 写操作
        NSBlockOperation *writeOperation = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"写操作开始....");
            sleep(2);
            NSLog(@"写操作结束");
        }];
        
        // 写之后的读操作
        NSBlockOperation *readOperationAfter = [[NSBlockOperation alloc] init];
        [readOperationAfter addExecutionBlock:^{
            NSLog(@"写之后的读操作1开始....");
            sleep(1);
            NSLog(@"写之后的读操作1结束");
        }];
        [readOperationAfter addExecutionBlock:^{
            NSLog(@"写之后的读操作2开始....");
            sleep(1);
            NSLog(@"写之后的读操作2结束");
        }];
        
        // 添加依赖关系;
        [writeOperation addDependency:readOperationBefore];
        [readOperationAfter addDependency:writeOperation];
        
        // 将操作加入队列;任务会自动启动
        NSOperationQueue *workQueue = [[NSOperationQueue alloc] init];
        [workQueue addOperations:@[readOperationBefore, writeOperation, readOperationAfter] waitUntilFinished:NO];
    }
    
    企业微信截图_d9dd1a86-6184-4845-b48f-ade6e080c7f6.png

    任务组

    • 将所有子任务都当做一个一个的block,加入一个workOperation

    • 将更新界面的操作放入mainOperation

    • 设置依赖关系:workOperation <= mainOperation

    • workOperation加入一个并行队列,普通队列,NSOperationQueue;
      mainOperation放入主队列NSOperationQueue.mainQueue

    // 任务组
    - (IBAction)groupButtonTouched:(id)sender {
        // 网络任务开始前,修改界面
        self.groupButton.enabled = NO;
        NSLog(@"网络组下载任务开始了... ...");
        
        // 将所有的下载子任务都放入一个operation
        NSBlockOperation *workOperation = [[NSBlockOperation alloc] init];
        [workOperation addExecutionBlock:^{
            NSLog(@"任务1开始...");
            sleep(2);
            NSLog(@"任务1结束");
        }];
        
        [workOperation addExecutionBlock:^{
            NSLog(@"任务2开始...");
            sleep(1);
            NSLog(@"任务2结束");
        }];
        
        [workOperation addExecutionBlock:^{
            NSLog(@"任务3开始...");
            sleep(3);
            NSLog(@"任务3结束");
        }];
        
        // 更新界面操作放入一个operation
        NSBlockOperation *mainOperation = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"完成所有子任务,更新界面");
            self.groupButton.enabled = YES;
        }];
        
        // 添加依赖关系
        [mainOperation addDependency:workOperation];
        
        // 将操作加入队列;任务会自动启动
        NSOperationQueue *workQueue = [[NSOperationQueue alloc] init];
        [workQueue addOperation:workOperation];
        [NSOperationQueue.mainQueue addOperation:mainOperation];
    }
    
    企业微信截图_741ce1ed-b61c-493f-a2c7-54b65e1bbf2a.png

    线程安全

    • NSOperationQueue没有线程安全相关的属性或者方法

    • 线程安全可以借助NSLock实现

    • 场景模拟:上海,北京,杭州三个站一起开始卖火车票

    • 卖票过程是需要线程安全的。对于票数ticketNumber,无论读写都要求线程安全。

    // 卖票;name:售票窗口名; number:售票数量
    - (void)saleTicketWithName:(NSString *)name andNumber:(NSInteger)number {
        // 加锁,保护self.ticketNumber
        [self.lock lock];
        
        if (self.ticketNumber >= number) {
            
            // 售票时间
            [NSThread sleepForTimeInterval:0.5];
            
            self.ticketNumber -= number;
            
            NSLog(@"%@窗口售出%ld张火车票,剩余票数:%ld", name, (long)number, (long)self.ticketNumber);
            
        } else {
            NSLog(@"%@窗口售票%ld张失败,剩余票数:%ld", name, (long)number, (long)self.ticketNumber);
        }
        
        // 解锁
        [self.lock unlock];
    }
    
    • 卖票过程是并行的,更新界面需要在主线程中,这里存在依赖关系
      saleOperation <= uiOperation
    // 线程安全
    - (IBAction)safeButtonTouched:(id)sender {
        // 开始售票
        self.safeButton.enabled = NO;
        self.ticketNumber = 30;
        NSLog(@"售票开始了,一共有%ld张票", (long)self.ticketNumber);
        
        // 所有的售票活动都放入saleOperation
        NSBlockOperation *saleOperation = [[NSBlockOperation alloc] init];
        
        // 售票点1
        [saleOperation addExecutionBlock:^{
            for (int i = 0; i < 4; i++) {
                NSInteger number = 1 + arc4random() % 5;
                [self saleTicketWithName:@"上海站" andNumber:number];
                [NSThread sleepForTimeInterval:1];
            }
        }];
        
        // 售票点2
        [saleOperation addExecutionBlock:^{
            for (int i = 0; i < 3; i++) {
                NSInteger number = 1 + arc4random() % 5;
                [self saleTicketWithName:@"北京站" andNumber:number];
                [NSThread sleepForTimeInterval:1];
            }
        }];
        
        // 售票点3
        [saleOperation addExecutionBlock:^{
            for (int i = 0; i < 3; i++) {
                NSInteger number = 1 + arc4random() % 5;
                [self saleTicketWithName:@"杭州站" andNumber:number];
                [NSThread sleepForTimeInterval:1];
            }
        }];
        
        // 界面更新放入uiOperation
        NSBlockOperation *uiOperation =  [NSBlockOperation blockOperationWithBlock:^{
            self.safeButton.enabled = YES;
            NSLog(@"售票结束,剩余%ld张票", (long)self.ticketNumber);
        }];
        
        // 建立依赖; 先售票,再更新界面
        [uiOperation addDependency:saleOperation];
        
        // 添加队列,并启动
        NSOperationQueue *saleQueue = [[NSOperationQueue alloc] init];
        [saleQueue addOperation:saleOperation];
        [NSOperationQueue.mainQueue addOperation:uiOperation];
    }
    
    企业微信截图_e6257b67-2997-4917-a5da-b73167e709dc.png

    自定义NSOPeration

    NSBlockOperation已经足够好用,实在有需要,可以考虑自定义NSOPeration

    iOS开发 自定义NSOPeration

    吐槽

    • 从整体设计角度来讲,比较差,原因是范围过大,结构复杂,比较难理解。

    • 分为队列,操作,任务三级,层级较多。

    • 任务又分为block,Selector,自定义三种,过于复杂。

    • 以最大并发数量为1来表示串行队列,不够直观。

    小结

    • 尝试过之后,确实是在GCD的基础上进行了封装,减少了很多细节

    • 任务固定为block,放弃Selector和自定义,大多数情况下是可行的。只要用好block就可以了,这样就简单直接了。

    • 队列就分为自定义的并行队列和全局队列两种,这样就简单了。除了更新界面的事情,其他都放自定义的并行队列中。只有一些特殊场景,比如两个写操作,需要用到串行队列,那么就把队列的最大并发数量设置为1,其他情况,都不需要管这个属性。

    • 有顺序要求,就考虑依赖,基本上可以解决运行顺序问题。

    • 线程安全,就引入NSLock,这个用起来也简单。

    • 简化之后,多线程就考虑用NSOperation、NSOperationQueue-,这个比GCD用起来方便,功能也多一点。

    参考文章

    iOS 多线程:『NSOperation、NSOperationQueue』详尽总结

    Demo地址

    NSOperationDemo

    相关文章

      网友评论

          本文标题:iOS话题:NSOperation、NSOperationQue

          本文链接:https://www.haomeiwen.com/subject/hfktghtx.html