美文网首页
多线程之NSThread/GCD/NSOperation

多线程之NSThread/GCD/NSOperation

作者: 那抹浮沉 | 来源:发表于2020-07-21 23:36 被阅读0次
    • 概述及基本概念
      1.进程和线程
      2.多线程
      3.任务
      4.队列
      5.iOS中的多线程技术
      6.GCD和NSOperationQueue的对比
      7.使线程同步的方法
    • NSThread
      1.创建和使用
      2.线程安全问题
      3.线程间通信
    • GCD
      1.串行/并行,同步/异步
      2.创建/获取队列
      3.线程间通信
      4.GCD常用函数
    • NSOperation
      1.常规创建
      2.添加操作到队列
      3.自定义NSOperation
    • NSThread+runloop实现常驻线程

    概述

    1.进程和线程

    • 进程
      一个运行起来的程序就是一个进程,每个进程之间是独立的,拥有运行所需的全部资源
    • 线程
      程序执行流的最小单元,是进程中的一个实体,一个进程至少有一个线程
    • 关系
      进程的任务是在线程中执行的,进程内,线程共享资源

    2.多线程

    • 什么是多线程
      同一时间内,cup只能处理一条线程,多线程并发其实就是cpu快速的在线程之间调度/切换,造成了多线程的假象
    • 多线程的优点
      1.提高程序的执行效率
      2.充分利用设备的多核,提高资源利用率(cpu,内存...)
    • 多线程的缺点
      1.开启线程需要占用一定的内存空间(主线程:1M,子线程512KB, iOS下创建线程大约需要90毫秒的时间),大量开启会占用大量内存空间,降低程序的性能
      2.如果线程非常多,cpu在N多线程之间调度,消耗大量cpu资源,会降低线程的执行效率
      3.程序设计更加复杂:线程间通信,多线程数据共享
    • 特点
      1.多线程执行顺序不确定,有name属性方便确定bug点
      2.优先级不能保证完全优先,只是大概率优先,执行完自动销毁,

    3.任务

    • 任务
      我们要执行的代码(块)
    • 同步sync
      不具备开启线程的能力,将同步任务添加到指定队列中,任务执行完毕之前会阻塞线程
    • 异步async
      具备开启新线程的能力,无需等待继续执行下方的任务,不会阻塞线程,无法确认任务的执行顺序

    4.队列

    • 队列
      存放任务的线性表,FIFO(先进先出)
    • 串行队列
      同一时间内,队列中只能执行一个任务(只开启了一个线程),按照任务添加的顺序,一个任务执行完才能执行下一个任务((仅是队列内串行,可以多个串行队列并行))
    • 并发队列
      同一时间内,允许多个任务并发执行(可以开启多个线程),任务之间不会互相等待,且这些任务的执行顺序和执行过程是不可预测的,只有在异步函数下才有效

    5.iOS中的多线程技术

    //生命周期,线程任务执行完毕后释放,而不是出了作用域释放

    • pThread(c),使用难度大,需要手动管理线程的生命周期(启动、回收...)
    • NSThread(oc),使用简单且灵活,手动管理(performSelector开辟的子线程也是NSThread的另一种体现方式)
    • GCD(c)充分利用设备的多核,自动管理
    • NSOperation(oc)自动管理,封装了GCD,多了一些实用功能,更面向对象

    6.GCD和NSOperationQueue的对比

    1.GCD执行的是由block构成的任务,执行效率更高
    2.GCD只支持FIFO,而NSOperationQueue可以通过设置并发数、优先级,依赖等调整执行顺序
    //所以GCD高效,NSOperationQueue多能,根据需要使用
    3.NSOperationQueue可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂

    4.NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)

    7.使线程同步的方法

         0.加锁 / 阻塞任务
         1.nsopreation的依赖关系 / 设置最大并发数
         2.GCD的 dispath_group   / 信号量机制,栅栏
    

    8.自旋锁与互斥锁

    • 常见的自旋锁:atomic
      A线程在执行任务时,B会不听的在外面徘徊,一旦A执行完毕,B立马执行
    • 常见的互斥锁:@synchronized、NSLock
      A线程在执行任务时,B会进入休眠状态,A执行完毕后,B会自动唤醒去执行
    • 对比
      1.自旋锁的执行效率高于互斥锁
      2.如果无法在短时间内获取锁,自旋锁一直占用cup,会降低cpu的执行效率

    这里提一下原子属性:atomic(自旋锁-默认为setter方法加锁)

    一样会消耗大量的资源,建议使用noatomic,因为开发中出现资源抢夺的可能性极低,也可以尽量将加锁,资源抢夺的业务交给服务端处理

    • 死锁
      死锁原因: 函数未返回阻塞当前任务 + 队列中任务无法并发执行
      解决死锁:1.用异步函数 2.新建异步队列不和main一个队列
    //pragma mark -- 常见的死锁
    //死锁原因:viewDidLoad的任务也是在主队列上的,由于队列的先进先出原则
    /*viewDidLoad 来了-- dispatch_sync 来了
    viewDidLoad想要执行完走人,viewDidLoad 就得执行完毕,
    这时候添加了dispatch_sync,根据FIFO原则,viewDidLoad执行完,dispatch_sync才能走,
    但是(串行)viewDidLoad 没执行完,就不会执行dispatch_sync,就造成了死锁
    */
    //想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSLog(@"");
    
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"");
        });
        NSLog(@"");
    }
    

    扩展
    不管同步还是异步,在同一个队列中添加任务,就是串行,就会造成阻塞

    NSThread

    1.创建和使用

        //(4个状态:创建,就绪,阻塞,结束)
        //实例方法,创建
        NSThread * thred1 = [[NSThread alloc]initWithTarget:self selector:@selector(hh) object:nil];
        //设置线程的属性要在start之前
        thred1.name = @"thred1";
        //优先级
        thred1.threadPriority = 1;//To be deprecated; use qualityOfService below
        [thred1 start];//运行/就绪切换
        thred1.qualityOfService = 1; //read-only after the thread is started
    
        [NSThread sleepForTimeInterval:1];//阻塞方式1
        [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];//阻塞方式2
        [NSThread exit];//手动强制结束,
    
        //  获取/判断线程
        [NSThread mainThread];
        [NSThread isMainThread];
        [NSThread currentThread];
    
        //下面2种创建线程的方法简单,但是无法拿到线程对象,进行设置优先级等
        //类方法-分离子线程,不需要start
        [NSThread detachNewThreadSelector:@selector(hh) toTarget:self withObject:nil];
    
        //隐式创建后台线程(开辟子线程)
        [self performSelectorInBackground:@selector(hh) withObject:nil];
    
    /*
        //当前线程延迟1秒执行(使用带有参数afterDelay的方法,内部会创建一个NSTimer添加到当前线程中,如果在子线程中,需要开启runloop)
        [[NSRunLoop currentRunLoop] run];
        [self performSelector:@selector(showImage:) withObject:nil afterDelay:1.0];
        //在指定线程执行,waitUntilDone :是否等待方法内代码执行完毕后再去执行本行代码后面的代码
        [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
        //主线程执行
        [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
    */
    

    2.线程安全问题

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        //数据安全问题,多线程访问同一资源
        self.count = 10;
        self.threadA = [[NSThread alloc]initWithTarget:self selector:@selector(mai) object:nil];
        self.threadB = [[NSThread alloc]initWithTarget:self selector:@selector(mai) object:nil];
        self.threadC = [[NSThread alloc]initWithTarget:self selector:@selector(mai) object:nil];
        self.threadA.name = @"threadA";
        self.threadB.name = @"threadB";
        self.threadC.name = @"threadC";
        [self.threadA start];
        [self.threadB start];
        [self.threadC start];
        
    }
    
    - (void)mai
    {
        while (1) {
            
            if (self.count > 0) {
                
                //添加一个耗时操作,让卖票员手速变慢
                for (int i = 0; i < 1000000; i++) {
                    int a = 1+i;
                }
                
                self.count--;
                NSLog(@"%@卖出了一张票,还剩下%d张",[NSThread currentThread].name,self.count);
            }else{
                NSLog(@"mei");
                break;
            }
        }
    }
    

    输出结果:(这样目测更清晰)
    [2427:109348] threadA卖出了一张票,还剩下9张
    [2427:109350] threadC卖出了一张票,还剩下8张
    [2427:109349] threadB卖出了一张票,还剩下7张
    [2427:109348] threadA卖出了一张票,还剩下6张
    [2427:109350] threadC卖出了一张票,还剩下5张
    [2427:109349] threadB卖出了一张票,还剩下4张
    [2427:109350] threadC卖出了一张票,还剩下3张
    [2427:109348] threadA卖出了一张票,还剩下2张
    [2427:109349] threadB卖出了一张票,还剩下1张
    [2427:109350] threadC卖出了一张票,还剩下0张
    [2427:109350] mei
    [2427:109348] threadA卖出了一张票,还剩下-1张
    [2427:109348] mei
    [2427:109349] threadB卖出了一张票,还剩下-2张
    [2427:109349] Mei
    由上可以看出,由于线程默认是异步的,资源竞争时,会发生我们不想看到的问题
    那么接下来解决问题,添加互斥锁

    - (void)mai
    {
        while (1) {
            //互斥锁,实现线程同步,但是会小号大量CPU资源
            @synchronized (self) {
                if (self.count > 0) {
                    
                    //添加一个耗时操作,让卖票员手速变慢
                    for (int i = 0; i < 1000000; i++) {
                        int a = 1+i;
                    }
                    
                    self.count--;
                    NSLog(@"%@卖出了一张票,还剩下%d张",[NSThread currentThread].name,self.count);
                }else{
                    NSLog(@"mei");
                    break;
                }
            }
        } 
    }
    

    3.线程间通信

    以下载图片为例,贴代码了,不过多解释

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {    //线程间通信
        [NSThread detachNewThreadSelector:@selector(downLoadImage) toTarget:self withObject:nil];
    }
    
    - (void)downLoadImage
    {
        NSURL *url = [NSURL URLWithString:@"https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2567670815,24101428&fm=26&gp=0.jpg"];
        
        //计算下载时间,方法随意,这是其一
        NSDate * std = [NSDate date];
        NSData *imageData = [NSData dataWithContentsOfURL:url];
        NSDate * end = [NSDate date];
        NSLog(@"%f",[end timeIntervalSinceDate:std]);//0.404483
        
        UIImage *image = [UIImage imageWithData:imageData];
        
        //一个简单的做法(performSelector方法,可以由任何继承nsobject的对象调用)
        [self.imagev performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
      
    }
    
    - (void)showImage:(UIImage *)image
    {
          
        self.imagev.image = image;
    }
    

    GCD

    1.串行/并行,同步/异步

    • 由此,两两组合,有4中情形(过于基础,有兴趣的自己尝试)
      1.异步+并行:多个任务会开启多个线程,队列中的任务的并行的
      2.异步+串行:多个任务只会开启1个线程,队列中的任务的串行的
      3.同步+并行:多个任务不会开启线程,任务是串行的(在主线程)
      4.同步+串行:多个任务不会开启线程,任务是串行的(在主线程)
      注意:
      1.并不是有多少个任务就开启多少个线程,由系统决定
      2.异步+主队列:所有任务都在主队列中执行,不会开启新的线程
      3.同步+主队列:死锁

    2.创建/获取队列

    GCD有3种队列类型
    1.mainqueue:通过dispatch_get_main_queue()获得,这是一个与主线程相关的串行队列
    2.globalqueue:全局队列是并发队列,由整个进程共享。存在着高、中、低三种优先级的全局队列。调用dispath_get_global_queue
    3.自定义队列:通过函数dispatch_queue_create创建的队列

    • 创建队列
    //创建并发和串行队列:2个参数,一个标记字符串, 一个串行还是并发的宏
        dispatch_queue_t concurrent_queue = dispatch_queue_create(@"demo_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t serial_queue = dispatch_queue_create(@"demo_serial_queue", DISPATCH_QUEUE_SERIAL);
    
    • 获取队列(本身存在队列)
    //获取主队列(串行) 及 常用的全局队列(并发)
        dispatch_queue_t main_queue = dispatch_get_main_queue();
        ////2个参数, 一个是优先级, 另一个是 : 保留供将来使用的标志。总是为这个参数指定0。
        dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    

    3.线程间通信

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSURL *url = [NSURL URLWithString:@"https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2567670815,24101428&fm=26&gp=0.jpg"];
            NSData *imageData = [NSData dataWithContentsOfURL:url];
            UIImage *image = [UIImage imageWithData:imageData];
            dispatch_async(dispatch_get_main_queue(), ^{
                self.imagev.image = image;
            });
        });
    }
    

    4.GCD常用函数

    • 1.延迟
        //5. dispatch_after 延时,内部使用的是dispatch_time_t 管理时间,子线程中不用关心runloop是否开启
        dispatch_time_t d = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * 3.0));
        dispatch_after(d, dispatch_get_main_queue(), ^{
            NSLog(@"hhh");
        });
    

    相对于 performSelector 和 NSTimer,它的优点是,可以控制在主线程还是子线程执行

    • 2.单例
            static objectA * ob = nil;
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                ob = [[objectA alloc]init];
            });
            reture ob;
    
    • 3.栅栏函数
        //栅栏函数:先执行前两个函数,然后执行完栅栏函数才执行后续函数(不能使用全局并发队列)
        //dispatch_barrier_sync上的队列要和需要阻塞的任务在同一队列上,否则是无效的。
        dispatch_queue_t concurrent_queue = dispatch_queue_create(@"concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(concurrent_queue, ^{
            NSLog(@"0000");
        });
        dispatch_async(concurrent_queue, ^{
            NSLog(@"1111");
        });
        //栅栏函数(遵循同步/异步规则)
        dispatch_barrier_sync(concurrent_queue, ^{
            NSLog(@"++++++++");
        });
        dispatch_async(concurrent_queue, ^{
            NSLog(@"2222");
        });
    
    • 4.快速迭代
        //快速迭代(类似for循环:主线程操作),会开启子线程完成任务,任务的执行是并发的,效率更高
        //参数:遍历的次数  队列(并发) 索引
        dispatch_apply(100, dispatch_get_global_queue(0, 0), ^(size_t ind) {
            //场景比如:大量文件操作
        });
    
    • 5.组队列
        //dispatch_group_t 监听任务的执行情况
        //等待一组操作都完成后执行后续操作(如大图分成几块下载,下载后拼接)
        dispatch_queue_t global_queu = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_group_t grou = dispatch_group_create();
        dispatch_group_async(grou, global_queu, ^{/*操作1*/});
        dispatch_group_async(grou, global_queu, ^{/*操作2*/});
        dispatch_group_async(grou, global_queu, ^{/*操作3*/});
        //拦截通知:当队列组中的任务都执行完毕时,会执行这个方法
        dispatch_group_notify(grou, dispatch_get_main_queue(), ^{
            /*后续操作-合并图片0绘图*/
        });
        /*
        dispatch_group_notify(grou, global_queu, ^{
            //后续操作-合并图片-绘图
            dispatch_async(dispatch_get_main_queue(), ^{
                //更新ui
            })
        });
        */
        //阻塞等待组内所有任务执行完毕后,执行之后的代码,效果同上dispatch_group_notify,栅栏函数一样也可以实现
        dispatch_group_wait(grou, DISPATCH_TIME_FOREVER);
    
    • 6.信号量:Dispatch Semaphore

    用途:(有需要自行研究)
    1.保持线程同步,将异步执行任务转换为同步执行任务
    2.保证线程安全,为线程加锁

    NSOperation

    1.常规创建

        //NSOperation 是基于GCD的一个抽象(基)类,将线程封装成要执行的操作,不需要管理线程的生命周期和同步,比GCD可控性更强,可以加入 操作依赖 控制操作的执行顺序,设置队列最大可并发执行的操作个数,取消操作等,GCD不能中途取消
        //NSBlockOperation 执行代码块
        //NSInvocationOperation 执行指定的方法
        //配置完成后便可以调用start函数在当前线程执行,如果要异步,加入NSOperationQueue中异步执行
        
        //NSInvocationOperation 常规使用
        //创建操作,在主线程中执行,类似于直接调用方法
        NSInvocationOperation * inv = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(hh) object:nil];
        [inv start];
    
        //NSBlockOperation 常规使用
        //可以后续添加block块,操作启动后会在不同线程并发执行这些执行快
        NSBlockOperation * bo = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"dd");//任务
        }];
        //追加任务,如果一个操作中的任务数大于1,那么会开子线程并发执行任务
        [bo addExecutionBlock:^{
            NSLog(@"aa");//任务
        }];
        [bo start];
    
    

    2.添加操作到队列

        //获取主队列
        NSOperationQueue * que = [NSOperationQueue mainQueue];
        //创建非主队列,具备并发+串行,默认是并发队列
        NSOperationQueue * quu = [[NSOperationQueue alloc]init];
        //可以通过设置最大并发数量(同一时间最多有多少任务可以执行)来设置串行,注意,串行!=只开一条线程,只是任务按序执行
      //默认值-1:表示最大值,大于1:并发,等于1:串行,等于0:不执行任务
        quu.maxConcurrentOperationCount = 1;
    
        //创建abc 3个操作
        NSOperation * a = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"o");
        }];
        NSOperation * b = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"m");
        }];
        NSOperation * c = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"g");
        }];
    /*
        //快捷创建操作并添加到队列
        [quu addOperationWithBlock:^{
            NSLog(@"addOperationWithBlock");
        }];
    */
        //添加操作依赖 c依赖于a b,c在a b完成后执行
        [c addDependency:a];
        [c addDependency:b];
        //添加操作到队列
        [que addOperation:c];
        [que addOperation:b];
        [que addOperation:a];
    

    3.自定义NSOperation

    1.自定义一个继承于NSOperation的子类,实现main方法

    //告诉要执行的任务是什么,自动调用
    -(void)main
    {
        NSLog(@"开线程了吗 %@",[NSThread currentThread]);
        //[980:25414] 开线程了吗 <NSThread: 0x600003cf7ac0>{number = 7, name = (null)}
        //[980:25411] 开线程了吗 <NSThread: 0x600003cdddc0>{number = 5, name = (null)}
    
    }
    

    2.调用

    - (void)yshOperation
    {
        //创建操作/封装性,提高复用性,不需要关注内部实现
        YSHOperation * ysho1 = [[YSHOperation alloc]init];
        YSHOperation * ysho2 = [[YSHOperation alloc]init];
        //创建队列
        NSOperationQueue * oq = [[NSOperationQueue alloc]init];
        //添加操作
        [oq addOperation:ysho1];
        [oq addOperation:ysho2];
    }
    

    4.常用属性及方法

        que.suspended = YES/NO;//暂停/恢复
        [que cancelAllOperations];
        //值得一提的是
        //自定义NSOperation中,main方法内为一个任务,无法对此方法中的操作进行 暂停和取消
        //内部有多个耗时任务(for循环大量数据)时,如果想要取消,在耗时操作(for循环)后添加,不建议在for循环内判断,耗费性能
    
        /*
         if (self.isCancelled) {//cancelAllOperations内部会判断这个属性,yes则取消
             return;
         }
         */
    

    NSThread+runloop实现常驻线程

    由于每次开辟子线程都会消耗cpu,在需要频繁使用子线程的情况下,频繁开辟子线程会消耗大量的cpu,而且创建线程都是任务执行完成之后也就释放了,不能再次利用,那么如何创建一个线程可以让它可以再次工作呢?也就是创建一个常驻线程
    那么我们可以用GCD实现一个单例来保存NSThread

    + (NSThread *)shareThread
    {
        static NSThread * shareThread = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            shareThread = [[NSThread alloc]initWithTarget:self selector:@selector(shareThread) object:nil];
            [shareThread start];
        });
        return shareThread;
    }
    
    - (void)shareThread
    {
        NSRunLoop * runl = [NSRunLoop currentRunLoop];
        [runl addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        [runl run];
        
    }
    

    相关文章

      网友评论

          本文标题:多线程之NSThread/GCD/NSOperation

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