美文网首页iOS面试
OC-多线程的理解和使用(转)

OC-多线程的理解和使用(转)

作者: SK丿希望 | 来源:发表于2018-10-16 10:22 被阅读12次

    转自:https://www.jianshu.com/p/9e9316b52575

    谈到编程,就离不开多线程。多线程提升了系统资源的利用率,使得程序在相同时间单位里可以做更多的事情,是我们每个程序员都必须掌握的重要知识。

    • 什么是线程

    线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

    • 什么是进程

    进程(英语:process),是计算机中已运行程序的实体。进程为曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。若干进程有可能与同一个程序相关系,且每个进程皆可以同步(循序)或异步(平行)的方式独立运行。现代计算机系统可在同一段时间内以进程的形式将多个程序加载到内存中,并借由时间共享(或称时分复用),以在一个处理器上表现出同时(平行性)运行的感觉。同样的,使用多线程技术(多线程即每一个线程都代表一个进程内的一个独立执行上下文)的操作系统或计算机架构,同样程序的平行线程,可在多CPU主机或网络上真正同时运行(在不同的CPU上)。

    • 什么是程序

    程序(英语:procedure),指特定的一系列动作、行动或操作,而这些活动、动作或操作必须以相同方式运行,借此在相同环境下恒常得出相同的结果(例如紧急应变程序)。粗略而言,程序可以指一序列的活动、作业、步骤、决断、计算和工序,当它们保证依照严格规定的顺序发生时即产生所述的后果、产品或局面。一个程序通常引致一个改变。现在小孩也可以写程序。

    我们经常容易混淆或者不知道如何对这三个概念明确的分界,我这里有一个简单的区分方法:程序真正运行在计算机中的实体,就变成了进程,(正在运行的程序叫进程)而进程可以包含一个或多个线程。

    今天,我们谈谈在OC中,多线程的几种方案。其中pthread是纯C语言的一套线程管理方案,由于在iOS开发中基本上不会使用,我们这里不做讨论。我们重点讨论以下几种方案:

    • NSThread

    NSThread有两种创建方法,一种是类方法,一种是实例方法,类方法创建的时候需要确定好执行的任务,没有任何返回,它会自己创建一个新线程去执行指定任务,而用实例方法则需要我们手动开启这个线程。创建的时候我们既可以使用SEL也可以使用Block,为了简化代码,我们都使用Block来创建。

    //类方法创建
        [NSThread detachNewThreadWithBlock:^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"%d",i);
                [NSThread sleepForTimeInterval:1];
            }
        }];
        //实例方法创建
        NSThread *thread = [[NSThread alloc] initWithBlock:^{
            for (int i = 97; i < 102; i++) {
                NSLog(@"%c",i);
                [NSThread sleepForTimeInterval:1];
            }
        }];
        [thread start];
    

    运行结果:

    image.png

    我们可以看到这两个线程是完全异步的。运行几次,结果也不相同。

    值得注意的是,一个NSThread线程的启动,有三种方式,我们前面已经看到了两种,一种是类方法直接创建并启动,另一种是[thread start];方法,还有一种是[thread main];,但是苹果并不建议我们直接使用main方法,它可以在子类化的时候重写并实现你的线程主体,而不用调用super,任何时候,启动线程都应该用start方法。

    前面我们简单得用NSThread的方式创建了两个线程,并且让它们各自执行了自己的任务。但是NSThread可不止这么几个方法,它还有很多比较有用的方法:

        //设置名字
        [thread setName:@"myThread"];
        //设置优先级,由0到1.0的浮点数指定,其中1.0是最高优先级。
        [thread setThreadPriority:1];
        //退出当前线程
        [NSThread exit];
        //睡眠 单位是秒
        [NSThread sleepForTimeInterval:1];
        //获取当前线程
        [NSThread currentThread];
        //获取主线程
        [NSThread mainThread];
        //判断是否在主线程
        [NSThread isMainThread];
    

    接下来,我们用经典的卖票问题来模拟并解决NSThread中的线程同步问题。有两个售票员,同时开始卖票,一共有20张票,模拟该场景:

        @property (nonatomic, assign) NSInteger tickets;
    
        //初始有20张票
        self.tickets = 20;
    
        //创建两个线程来充当两个售票员
        [NSThread detachNewThreadWithBlock:^{
            while (self.tickets > 0) {
                [NSThread sleepForTimeInterval:1];
                self.tickets --;
                NSLog(@"还有%ld张票",(long)self.tickets);
            }
        }];
        [NSThread detachNewThreadWithBlock:^{
            while (self.tickets > 0) {
                [NSThread sleepForTimeInterval:1];
                self.tickets --;
                NSLog(@"还有%ld张票",(long)self.tickets);
            }
        }];
    

    运行结果:


    image.png

    通过打印结果,可以看出,一共卖出去了26张票,超出了我们的票的总数,这是同一张票被多次出售的结果。为了避免这种情况,通常的做法就是上锁,当某一条线程对数据进行操作时,先给数据上锁,别的线程阻塞,等到这条线程操作结束,在开锁,别的线程再进去,上锁,操作,开锁……这样就保证了数据的安全性。

    我们可以使用@synchronized (object) {}来进行上锁,括号里的参数可以填任意对象,但是要注意的是,必须填写线程共有的变量才能实现上锁,局部变量是无效的,原因是,如果用局部变量,就会创建多个锁,这些锁之间并无关联,所以与不上锁没有区别:

    //创建两个线程来充当两个售票员
        [NSThread detachNewThreadWithBlock:^{
            //对卖票过程加锁
            @synchronized (self) {
                while (self.tickets > 0) {
                    [NSThread sleepForTimeInterval:1];
                    self.tickets --;
                    NSLog(@"还有%ld张票",(long)self.tickets);
                }
            }
        }];
        [NSThread detachNewThreadWithBlock:^{
            //对卖票过程加锁
            @synchronized (self) {
                while (self.tickets > 0) {
                    [NSThread sleepForTimeInterval:1];
                    self.tickets --;
                    NSLog(@"还有%ld张票",(long)self.tickets);
                }
            }
        }];
    

    运行结果:


    image.png

    因为在锁内进行数据操作时,其它线程都会阻塞在外面,这个时候,其实线程不是并发执行的,所以我们不难想到,锁内执行的任务越少,那么这段代码执行的效率就越高。在此基础上,我们可以对前面的加锁进行一个小修改:

    //创建两个线程来充当两个售票员
        [NSThread detachNewThreadWithBlock:^{
            //对卖票过程加锁
            while (true) {
                [NSThread sleepForTimeInterval:1];
                @synchronized (self) {
                    if (self.tickets < 1) {
                        break;
                    }
                    self.tickets --;
                    NSLog(@"还有%ld张票",(long)self.tickets);
                }
            }
        }];
        [NSThread detachNewThreadWithBlock:^{
            //对卖票过程加锁
            while (true) {
                [NSThread sleepForTimeInterval:1];
                @synchronized (self) {
                    if (self.tickets < 1) {
                        break;
                    }
                    self.tickets --;
                    NSLog(@"还有%ld张票",(long)self.tickets);
                }
            }
        }];
    

    运行结果:


    image.png

    注意看一下时间,修改以后,我们的卖票效率提升了一倍,之前那种方式要20秒才能卖完,现在只需要10秒。

    当然也可以用NSLock来进行上锁,使用NSLock需要创建一个NSLock实例,然后调用lockunlock方法来进行加锁和解锁的操作:

    @property (nonatomic, strong) NSLock *lock;
    
        //初始化锁
        self.lock = [[NSLock alloc] init];
    
        //创建两个线程来充当两个售票员
        [NSThread detachNewThreadWithBlock:^{
            //对卖票过程加锁
            while (true) {
                [NSThread sleepForTimeInterval:1];
                [self.lock lock];
                if (self.tickets < 1) {
                    break;
                }
                self.tickets --;
                NSLog(@"还有%ld张票",(long)self.tickets);
                [self.lock unlock];
            }
        }];
        [NSThread detachNewThreadWithBlock:^{
            //对卖票过程加锁
            while (true) {
                [NSThread sleepForTimeInterval:1];
                [self.lock lock];
                if (self.tickets < 1) {
                    break;
                }
                self.tickets --;
                NSLog(@"还有%ld张票",(long)self.tickets);
                [self.lock unlock];
            }
        }];
    

    运行结果跟上面是一样的。

    不知道你还记得不记得atomic,这个就修饰了属性的原子性,如果直接把属性修饰改为atomic,会不会就不需要我们加锁了呢?我试过,不行!这是因为atomic只会对该属性的GetterSetter方法上锁,而我们很显然是在别的方法里面对数据进行操作,所以并没什么卵用。同时也因为atomic太耗性能,所以在实际开发中,我们一般都不使用它来修饰变量。

    • GCD

    • Grand Central Dispatch (GCD)是什么?

    GCD中文翻译过来是宏伟的中枢调度,是一种基于C语言的并发编程技术。它是苹果为多核的并行运算提出的解决方案,会自动调度系统资源,所以它的效率很高。
    GCD并不直接操作线程,而是操作队列和任务。我们只需要把任务添加到队列里,然后指定任务执行的方式,GCD就会自动调度线程执行任务。

    GCD的任务都是以Block形式存在的。

    • 队列有两种:串行队列/并发队列。

    串行队列只能等一个任务执行完毕才可以继续调度下一个任务

        /*  创建一个串行队列
         *  参数:1.名字2.类型,DISPATCH_QUEUE_SERIAL(串行队列) DISPATCH_QUEUE_CONCURRENT(并发队列)
         */
        dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    

    并发队列可以同时调度多个任务

    /*  创建一个并发队列
         *  参数:1.名字2.类型,DISPATCH_QUEUE_SERIAL(串行队列) DISPATCH_QUEUE_CONCURRENT(并发队列)
         */
        dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
    • 执行任务也有两种方式:同步执行/异步执行。

    同步执行会等待当前任务完成才会执行下一个任务,不会开启新线程。

        /*  同步执行任务
         *  参数:1.队列2.block(任务)
         */
        dispatch_sync(dispatch_queue_t  _Nonnull queue, ^(void)block);
    

    异步执行不会等待当前任务完成就会执行下一个任务,可以开启新线程(如果是主队列,则不会开启新线程,因为主队列的任务都会在主线程执行)。

        /*  异步执行任务
         *  参数:1.队列2.block(任务)
         */
        dispatch_async(dispatch_queue_t  _Nonnull queue, ^(void)block);
    

    队列和任务都有两种,排列组合以后就有四种情况,在不同的情况下,执行的结果可能会有差异,如果不清楚原理比较容易混淆。这里有一个简单的方法去分析执行情况:队列的类型决定了能不能同时执行多个任务(串行队列一次只能执行一个任务,并发队列一次可以执行多个任务),执行的方式决定了会不会开启新线程(同步执行不会开启新线程,异步执行可以开启新线程)。

    了解以上的基础,我们就可以利用GCD进行编程了,我们把创建好的队列两个队列分别放到两种执行方式中,把这四种情况都演示一遍,任务都是在block中打印0-9:

    1.同步执行串行队列任务
        //创建一个串行队列
        dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    
        //同步执行串行队列任务
        for (int i = 0; i < 10; i ++) {
            dispatch_sync(serialQueue, ^{
                NSLog(@"%d %@",i,[NSThread currentThread]);
            });
        }
    

    运行结果:


    image.png

    我们可以看到,同步执行的方式并没有开启新线程,打印结果也是顺序的。

    2.同步执行并发队列任务
        //创建一个并发队列
        dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
        //同步执行并发队列任务
        for (int i = 0; i < 10; i ++) {
            dispatch_sync(concurrentQueue, ^{
                NSLog(@"%d %@",i,[NSThread currentThread]);
            });
        }
    

    运行结果:


    image.png

    可以看到同步执行的情况下,无论是串行队列还是并发队列,结果并没有区别,这是因为在同步执行的情况下并不会开启新的线程,所有任务都只能在一条线程上执行,而同一条线程上的任务只能串行执行,所以即使并发队列拥有同时调度多个任务的能力,但是在一条线程的情况下,也只能等前一个任务执行完毕再调度新的任务去执行。所以,在同步执行任务的情况下,串行队列和并发队列的运行结果是一致的。

    3.异步执行串行队列任务
        //创建一个串行队列
        dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    
        //异步执行串行队列任务
        for (int i = 0; i < 10; i ++) {
            dispatch_async(serialQueue, ^{
                NSLog(@"%d %@",i,[NSThread currentThread]);
            });
        }
    

    运行结果:


    可以看到,这种情况下,任务是顺序执行的,但是它是在子线程执行的。这是因为,异步执行可以开启新线程,但是由于是串行队列,所以任务只能一个一个顺序执行。

    4.异步执行并发队列任务
        //创建一个并发队列
        dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
        //异步执行并发队列任务
        for (int i = 0; i < 10; i ++) {
            dispatch_async(concurrentQueue, ^{
                NSLog(@"%d %@",i,[NSThread currentThread]);
            });
        }
    

    运行结果:


    image.png

    可以看到,这种情况下,执行任务的顺序不固定,并且会开启多条线程同时执行,所以这种时候执行任务的效率最高。

    在实际的开发中,我们更多运用到的还是异步执行,毕竟我们运用多线程技术是为了在另一条线程上执行任务,至于选择串行队列还是并发队列就要根据实际情况来判断了:

    如果队列里的任务必须按照顺序执行,那就选择串行队列。
    如果队列里的任务没有执行顺序的需求,那最好选择并发队列,因为并发队列的执行效率更高。
    系统也为我们提供了两种队列,分别是:全局队列dispatch_get_global_queue(long identifier, unsigned long flags)、主队列dispatch_get_main_queue()
    全局队列本质上是一个并发队列,可以通过前面的测试来证明,获取时需要传递参数,第一个参数是服务质量的选择(以前叫优先级),第二个是保留参数,暂时只需要传0就可以了:

        //全局队列1.优先级或服务质量,2.保留参数,目前传0
        /*
         *  优先级和服务质量的对应关系:
         *  - DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
         *  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
         *  - DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
         *  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND
         */
        //默认优先级的全局队列
        dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    

    这里值得注意的是,一般情况下最好不要随意选择优先级,默认就够用了。优先级本质上是一个概率的问题,优先级越高,CPU调度的概率越高,并不具备确定性,如果出现问题,很难查找原因。当然,如果你很了解这些,并且就是为了性能和资源的考虑而做了优先级的选择,那么你可以无视这些。

    主队列不需要参数可以直接获取,不过主队列并不会开启新线程,主队列上的所有任务都只会在主线程上执行,所以我们在平时的编程中,往往是在子线程中处理耗时操作,然后在主线程更新UI。在实际开发中,我们经常会遇到一种场景,就是在界面上显示一张网络图片,要显示图片肯定得先下载,而下载是一个耗时操作,如果在主线程下载,那就会使界面卡住不能进行其它操作,所以我们一般都会在子线程下载,下载好以后再去主线程更新界面:

        dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
        dispatch_async(globalQueue, ^{
            NSLog(@"在子线程执行耗时操作!");
            dispatch_async(mainQueue, ^{
                NSLog(@"在主线程更新UI");
            });
        });
    

    以上的使用可以满足一些简单业务的需求,但是实际开发中有很多复杂业务,比如说在用户登录的时候需要同步多种信息,而这些信息从不同的接口获取,只有所有信息全部同步结束才可以正常操作,同步各种信息应该各自在子线程进行,我们可以异步执行并发队列中的任务来做这些耗时操作,但是我们怎么知道所有任务都执行完了呢?

    GCD Group

    GCD为我们提供了另一个东西,叫做Group(调度组)。调度组是用来协调一个或多个任务提交到队列异步触发的。 应用程序可以使用调度组等待所有调度组中的所有任务的完成。

    所有异步队列执行完毕后得到一个通知。

    调度组的使用并不复杂,它有两种用法:

        //创建一个调度组
        dispatch_group_t group = dispatch_group_create();
        //获取全局队列
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        //为队列添加任务,并且和给定的调度组关联
        dispatch_group_async(group, queue, ^{
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"同步信息1");
        });
        
        dispatch_group_async(group, queue, ^{
            NSLog(@"同步信息2");
        });
        
        dispatch_group_async(group, queue, ^{
            [NSThread sleepForTimeInterval:.5];
            NSLog(@"同步信息3");
        });
    
        //所有任务执行完毕通知
        dispatch_group_notify(group, queue, ^{
            NSLog(@"全部都完了");
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"更新UI");
            });
        });
    

    或者

        //创建一个调度组
        dispatch_group_t group = dispatch_group_create();
        //获取全局队列
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        //手动添加一个任务到该调度组
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"同步信息1");
            //该任务执行完毕从调度组移除
            dispatch_group_leave(group);
        });
        
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"同步信息2");
            dispatch_group_leave(group);
        });
        
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:.5];
            NSLog(@"同步信息3");
            dispatch_group_leave(group);
        });
    
        //等待所有任务执行完毕 参数:1.对应的调度组 2.超时时间
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        //所有任务执行完毕才会来这里
        dispatch_async(queue, ^{
            NSLog(@"全部都完了");
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"更新UI");
            });
        });
    

    运行结果:


    image.png

    两种用法的运行结果是相同的,第二种更加灵活一些,但是代码量也相应多一些,使用调度组我们可以更好的对任务进行控制,并且在特定的场景满足我们的需求。灵活使用调度组,可以让我们对线程同步控制更加得心应手。

    GCD还有一个很重要的功能,就是一次执行。用这个代码块包含的代码只会执行一次,在实际开发中经常使用,单例模式一般都会用GCD来做,因为它效率高:

        for (int i = 0; i < 10; i++) {
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                NSLog(@"你猜我会执行几次?");
            });
        }
    

    运行以后只会打印一次。

    • NSOperation

    NSOperation是对GCD面向对象的封装。它拥有GCD的高效,也拥有面向对象的编程思想。和GCD类似,它也是把任务放在队列里去执行,不过它比GCD少了一些概念,但是它也有了一些GCD没有的功能,接下来我们就从最开始了解NSOperation
    Operation翻译过来是操作的意思,其实跟GCD的任务是一样的。因为它本身就是GCD的封装,所以在理解上也差不多。我们顺着GCD的用法来使用NSOperation
    NSOperation本身是个抽象类,我们要使用它就得使用它的子类。系统给我们提供了两个,分别是:NSInvocationOperationNSBlockOperation。一种是通过selector的形式添加操作,一种是以block的形式添加操作,我个人更喜欢NSBlockOperation,用起来更方便些。
    分别用这两种方式创建两个操作(其实就是GCD的任务):

        //NSBlockOperation
        NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"blockOperation");
        }];
        //NSInvocationOperation
        NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationMethod) object:nil];
    
    - (void)invocationMethod{
        NSLog(@"invocationOperation%@",[NSThread currentThread]);
    }
    

    根据GCD的操作流程,这时候就需要创建队列了。NSOperation的队列也有一个类NSOperationQueue,它的创建也很简单:

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

    然后把前面创建的操作添加到队列中:

        [operationQueue addOperation:blockOperation];
        [operationQueue addOperation:invocationOperation];
    

    这个地方也不需要指定它的执行方式,直接把操作添加到队列中就会自动异步执行:


    image.png

    NSOperationQueue本身也有通过block添加操作的方法,不需要我们专门去创建,这样就进一步简化了代码:

        NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
        
        [operationQueue addOperationWithBlock:^{
            NSLog(@"1%@",[NSThread currentThread]);
        }];
        [operationQueue addOperationWithBlock:^{
            NSLog(@"2%@",[NSThread currentThread]);
        }];
    

    直接运行:

    image.png
    NSOperationQueue并没有全局队列,但是我们可以自己根据需求创建全局队列。NSOperationQueue也有获取主队列的类方法[NSOperationQueue mainQueue];,用起来也很简单方便,跟GCD中的主队列一样。

    一个子线程处理耗时操作,然后刷新UI的代码,使用NSOperationQueue的方式就变成了这样:

        NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    
        [operationQueue addOperationWithBlock:^{
            NSLog(@"子线程处理耗时操作%@", [NSThread currentThread]);
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                NSLog(@"主线程更新UI%@", [NSThread currentThread]);
            }];
        }];
    

    运行结果:

    image.png

    怎么样,是不是用起来很简单。

    在线程同步上,NSOperation 没有group,但是有操作依赖,一样可以实现同样的效果。它的依赖,是操作的方法,所以如果要使用依赖,我们就得自己创建操作,然后操作之间设置好依赖关系,再把它们丢到队列里,比如说前面GCD中的那个同步信息的例子:

        NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"同步信息1");
        }];
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"同步信息2");
        }];
        NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:.5];
            NSLog(@"同步信息3");
        }];
        NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"更新UI");
        }];
        
        [op4 addDependency:op1];
        [op4 addDependency:op2];
        [op4 addDependency:op3];
    
        NSOperationQueue *queue = [NSOperationQueue new];
        
        [queue addOperations:@[op1,op2,op3,op4] waitUntilFinished:NO];
    

    这里,操作4依赖了操作1、2、3,所以它得等到其它3个操作完成才能开始执行,运行结果:


    image.png

    这里要注意,操作之间一定不能形成循环依赖,循环依赖的任务没办法执行,因为都得再对方执行完毕之后才满足自己的执行条件。

    这里在给队列添加操作的时候,用了新的方法[queue addOperations:@[op1,op2,op3,op4] waitUntilFinished:NO];,这个方法可以一次性添加多个操作,但是后面有一个参数,看名字我们就可以知道,如果传YES,它会一直等所有任务都执行完毕才会继续执行下面的任务,有点类似于GCD里面group的那个wait,但是它会卡主当前线程,所以不能在主线程中使用,我们也可以在子线程里面执行这一段代码,稍加改造,达到同样的效果:

        NSOperationQueue *queue = [NSOperationQueue new];
        
        [queue addOperationWithBlock:^{
            NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
                [NSThread sleepForTimeInterval:1.0];
                NSLog(@"同步信息1");
            }];
            NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
                NSLog(@"同步信息2");
            }];
            NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
                [NSThread sleepForTimeInterval:.5];
                NSLog(@"同步信息3");
            }];
            
            [queue addOperations:@[op1,op2,op3] waitUntilFinished:YES];
            
            NSLog(@"更新UI");
        }];
    

    运行结果是一样的,这里就不贴图了。

    不知道你有没有注意到,NSOperationQueue本身并没有并发队列和串行队列的选项,它默认是并发队列,但是,它有一个maxConcurrentOperationCount属性(代表了最大并发数,也就是最多能够开几条线程执行操作),如果最大并发数量为1,它就变成了类似串行队列的模样。

    NSOperationQueue还可以使用suspended属性来控制队列里操作的暂停和继续。使用cancelAllOperations方法来取消队列里的所有操作。这些简单的属性和方法就不专门演示了。

    • 总结

    以上就是OC中的多线程,在实际开发中,我们几乎不会使用pthread,很少会使用NSThread,不过NSThread的一些类方法会经常使用,比如获取当前线程,睡眠当前线程等。GCDNSOperation使用起来都效率更高,并且操作简单,是我们更好的选择。它俩之间的选择一般没有明确的分界线,可以根据实际需求来选择,不过一般中小型项目多使用GCD,大型项目多使用NSOperation,可能是因为GCD更底层,更轻一些,而NSOperation更规范,同时也重了些。

    与本文相关的代码点击前往

    相关文章

      网友评论

        本文标题:OC-多线程的理解和使用(转)

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