关于iOS多线程

作者: MiracleGl | 来源:发表于2017-01-11 18:13 被阅读910次

    本篇文章主要描述了多线程的一方面知识。可能有一些不足之处,希望大家指出。文章最后有一些关于多线程方面的面试题,仅供参考。

    本篇文章排版不是很好,可以去看 修改版

    多线程

    • 同步&异步
    • 进程&线程
    • 多线程基本概念
    • 多线程的优缺点

    同步&异步

    1 同步和异步是两种执行任务的方式。
    2 同步:代码从上到下顺序执行就叫做同步执行(多个任务依次执行)。
    3 异步:多个任务同时执行就是异步执行。

    线程&进程

    1 进程:在系统中正在运行的一个程序叫做进程,进程可以类比成正在正常运营的公司。
    2 线程:线程可以类比成公司里的员工,程序启动默认会开启一个线程。

    多线程的基本概念

    1 多线程:一个进程中可以开启多条线程,多条线程可以同时执行不同的任务。
    2 举个例子:酷我音乐的边下载边听歌,迅雷的边下载边播放。


    多线程的优缺点

    1 优点:能‘适当’提高程序的执行效率,能适当提高CPU的内存利用率,线程上的任务执行完成后,线程会自动销毁节省内存。
    2 缺点:如果开启线程过多会占用大量CPU资源降低程序性能。

    多线程的目的

    • 将耗时操作放在后台处理,保证UI界面的正常显示和交互。
    • 网络操作是非常耗时的,在做网络开发,所有网络访问都是耗时操作.需要在后台线程中执行。
    • 多线程开发的原则:越简单越好。

    常见的多线程几种方式

    • PThread(开发中几乎用不到)
    • NSThread(开发中很少使用到)
    • GCD(经常使用)
    • NSoperation(经常使用)


    GCD

    • GCD的概念
    • GCD的简单使用
    • GCD的任务和队列

    GCD的概念

    1 什么是GCD:全称是Grand Central Dispatch,纯C语言的,提供了非常多强大的函数。
    2 GCD的核心:将任务添加到队列。
    3 GCD使用的两个步骤:创建任务,确定要做的事情,GCD中的任务是使用BLOCK封装的。将任务添加到队列中,GCD会自动将队列中的任务取出,放到对应的线程中执行。任务的取出遵循队列的FIFO原则 : 先进先出,后进后出。

    GCD的简单使用

    1 任务添加到队列

    - (void)GCDDemo1
    {
        // 1. 创建队列
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        
        // 2. 创建任务 : 用block指定的 (无参无返回值的)
        void (^task)() = ^ {
            NSLog(@"%@",[NSThread currentThread]);
        };
        
        // 3. 把任务添加到队列
        // dispatch_async : 表示任务是异步的
        // dispatch_sync : 表示任务是同步的
        dispatch_async(queue, task);
    }
    

    2 简写

    - (void)GCDDemo2
    {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%@",[NSThread currentThread]);
        });
    }
    

    3 线程间的通信

    - (void)GCDDemo4
    {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            NSLog(@"假装在努力下载...%@",[NSThread currentThread]);
            // 下载结束之后,回到主线程更新UI
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"假装在更新UI...%@",[NSThread currentThread]);
            });
        });
    }
    

    4 使用GCD的线程间的通信实现异步下载网络图片

    - (void)downloadImage
    {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            NSLog(@"downloadImage %@",[NSThread currentThread]);
            
            // URL
            NSURL *URL = [NSURL URLWithString:@"http://atth.eduu.com/album/201203/12/1475134_1331559643qMzc.jpg"];
            // data
            NSData *data = [NSData dataWithContentsOfURL:URL];
            // image
            UIImage *image = [UIImage imageWithData:data];
            
            // 拿到图片对象之后,回到主线程刷新UI
            dispatch_async(dispatch_get_main_queue(), ^{
                
                NSLog(@"updateUI %@",[NSThread currentThread]);
                
                self.myImageView.image = image;
                [self.myImageView sizeToFit];
                [self.myScrollView setContentSize:image.size];
            });
        });
    }
    

    GCD的任务和队列

    1 GCD的任务:
    同步的方式执行任务 : 在当前线程中依次执行任务。
    dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
    异步的方式执行任务 : 新开线程在新线程中执行任务。
    dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
    2 GCD队列:
    串行队列:让任务一个接着一个有序的执行:不管队列里面放的是什么任务,一个任务执行完毕后,再执行下一个任务,同时只能调度一个任务执行。


    并发队列:可以让多个任务并发/同时执行,自动开启多个线程同时执行多个任务,同时可以调度多个任务执行。并发队列的并发功能只有内部的任务是异步任务时,才有效。

    代码小结

    1 串行队列+同步任务

    /*
     1.不开线程
     2.有序执行
    */
    - (void)GCDDemo1
    {
        /* 
         创建串行队列
         参数1 : 队列的标识符
         参数2 :  队列的属性,决定了队列是串行的还是并行的
         DISPATCH_QUEUE_SERIAL : 串行队列
        */
        dispatch_queue_t queue = dispatch_queue_create("GL", DISPATCH_QUEUE_SERIAL);
        
        // 循环的创建了10个同步任务,添加到队列
        for (NSInteger i = 0; i<10; i++) {
            
            // 把同步任务添加到串行队列
            dispatch_sync(queue, ^{
                NSLog(@"%zd %@",i,[NSThread currentThread]);
            });
        }
        
        NSLog(@"哈哈哈");
    }
    

    2 串行队列+异步任务

    - (void)GCDDemo2
    {
        // 串行队列
        dispatch_queue_t queue = dispatch_queue_create("GL", DISPATCH_QUEUE_SERIAL);
        
        for (NSInteger i = 0; i<10; i++) {
            // 把异步任务添加到串行队列
            dispatch_async(queue, ^{
                NSLog(@"%zd %@",i,[NSThread currentThread]);
            });
        }
        
        NSLog(@"嘿嘿嘿");
    }
    

    3 并行队列+同步任务

    /*
     不开线程
     有序执行
     */
    - (void)GCDDemo1
    {
        // 创建并行队列
        // DISPATCH_QUEUE_CONCURRENT : 并行队列
        // 并行队列只能决定"是否"可以同时调度多个任务;不能决定开不开线程
        dispatch_queue_t queue = dispatch_queue_create("GL", DISPATCH_QUEUE_CONCURRENT);
        
        for (NSInteger i = 0; i<10; i++) {
            // 把同步任务添加到并行队列
            dispatch_sync(queue, ^{
                NSLog(@"%zd %@",i,[NSThread currentThread]);
            });
        }
        
        NSLog(@"哈哈哈");
    }
    

    4 并行队列+异步任务

    /*
     开线程
     无序执行
     */
    - (void)GCDDemo2
    {
        // 并行队列
        dispatch_queue_t queue = dispatch_queue_create("GL", DISPATCH_QUEUE_CONCURRENT);
        
        for (NSInteger i = 0; i<10; i++) {
            
            // 把异步任务添加到并发队列
            dispatch_async(queue, ^{
                NSLog(@"%zd %@",i,[NSThread currentThread]);
            });
        }
        
        NSLog(@"嘿嘿嘿");
    }
    
    Paste_Image.png

    NSOperation

    • NSOperation的简介
    • NSOperation的简单使用
    • NSOperation的高级功能

    NSOperation的简介

    1 是OC语言中基于GCD的面向对象的封装,使用起来比GCD更加简单。提供了一些GCD不好实现的功能,苹果推荐使用。NSOperation还不用关心线程和线程的声明周期。
    2 NSOperation是个抽象类无法直接使用。因为方法只有声明没有实现。
    3 子类:NSInvocationOperation和NSBlockOperation,自定义NSOperation操作默是异步的。
    4 队列 : NSOperationQueue队列默认是并发的。
    5 核心:GCD的核心 : 将任务添加到队列中。OP的核心 : 将操作添加到队列中。

    NSOperation的简单使用

    1 先将需要执行的操作封装到一个NSOperation对象中,创建NSOperation对象。
    2 将NSOperation对象添加到NSOperationQueue中。
    3 NSOperationQueue会自动将NSOperation取出来。
    4 将取出的NSOperation封装的操作自动放到一条对应的新线程中执行。

    NSOperation的高级功能

    1 最大操作并发数。
    1>设置最大并发数

    // 队列的最大并发数的属性
    // 作用 : 控制队列同时调度任务执行的个数;
    // 间接控制了线程的数量;
    // 注意 : 队列的最大并发数,不是线程数;
    
    @implementation ViewController {
        
        /// 全局队列
        NSOperationQueue *_queue;
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        _queue = [[NSOperationQueue alloc] init];
        
        // 设置队列的最大并发数 : 至少开两个
        _queue.maxConcurrentOperationCount = 2;
    }
    

    2>演示

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        [self GCDDemo];
    }
    
    - (void)GCDDemo
    {
        for (NSInteger i = 0; i<50; i++) {
            
            NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
                NSLog(@"%zd %@",i,[NSThread currentThread]);
            }];
            
            [_queue addOperation:op];
        }
    }
    

    3>执行结果:任务是两个两个的执行。



    2 继续/暂停/取消全部。
    1>在最大并发数代码的基础上增加暂停、继续、取消。

    #pragma 取消全部
    /*
     1.正在执行的操作无法被取消;
     2.如果非要取消正在执行的操作,需要自定义NSOperation
     3.这个取消全部的操作有一定的时间延迟
     */
    - (IBAction)cancelAll:(id)sender
    {
        // 移除队列里面"所有"的操作
        [_queue cancelAllOperations];
        
        NSLog(@"取消全部 %tu",_queue.operationCount);
    }
    
    #pragma 继续
    - (IBAction)jixu:(id)sender
    {
        // 不挂起队列,使队列继续调度任务执行
        _queue.suspended = NO;
        
        NSLog(@"继续 %tu",_queue.operationCount);
    }
    
    #pragma 暂停
    /*
     1.正在执行的操作无法被暂停
     2.operationCount : 队列里面的操作个数;统计的是队列里面还没有执行完的操作;
     3.队列里面的任务一旦执行完,会从队列里面移除;
     */
    - (IBAction)zanting:(id)sender
    {
        // 挂起队列,使队列暂停调度任务执行
        _queue.suspended = YES;
        
        NSLog(@"暂停 %tu",_queue.operationCount);
    }
    

    2>暂停队列结论
    将队列挂起之后,队列中的操作就不会被调度,但是正在执行的操作不受影响。
    operationCount:操作计数,没有执行和没有执行完的操作,都会计算在操作计数之内。
    注意:如果先暂停队列,再添加操作到队列,队列不会调度操作执行。所以在暂停队列之前要判断队列中有没有任务,如果没有操作就不暂停队列。
    3>取消队列结论
    一旦调用的 cancelAllOperations方法,队列中的操作,都会被移除,正在执行的操作除外。
    正在执行的操作取消不了,如果要取消,需要自定义NSOperation。
    队列取消全部操作时,会有一定的时间延迟。
    3 操作间依赖关系。
    场景:登陆-->付费-->下载-->通知用户
    1>准备需要执行的操作

        // 登录
        NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"登录 %@",[NSThread currentThread]);
        }];
        
        // 付费
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"付费 %@",[NSThread currentThread]);
        }];
        
        // 下载
        NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"下载 %@",[NSThread currentThread]);
        }];
        
        // 通知用户
        NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"通知用户 %@",[NSThread currentThread]);
        }];
    

    2>添加依赖(核心代码)

        /*
         添加依赖关系
         1.不能在操作添加到队列之后,在建立依赖关系;因为已经晚了
         2.可以跨队列建立依赖关系
         3.不能建立循环依赖
         */
        [op2 addDependency:op1]; // 付费依赖登录
        [op3 addDependency:op2]; // 下载依赖付费
        [op4 addDependency:op3]; // 通知用户依赖下载
        
        // [op1 addDependency:op4]; // 登录依赖通知用户 : 循环依赖;会卡死
        
        // 批量把操作添加到队列
        // waitUntilFinished : 是否等待前面的异步任务执行完,在执行后面的代码
        [_queue addOperations:@[op1,op2,op3] waitUntilFinished:NO];
        
        // 一个操作不能同时添加到两个队列
        [[NSOperationQueue mainQueue] addOperation:op4];
    

    3>结论
    不能循环建立操作间依赖关系,否则队列不调度操作执行。
    操作间可以跨队列建立依赖关系。
    要将操作间的依赖建立好了之后,再添加到队列中,先建立操作依赖关系,再把操作添加到队列。


    面试题

    面试题仅供参考。 ><!

    1. 用 NSOpertion 和 NSOpertionQueue 处理 A,B,C 三个线程,要求执行完 A,B 后才能执行 C, 怎么做?
      答: 添加操作依赖,C依赖于A同时依赖于B。创建操作队列,将操作添加到操作队列中。
    2. 线程间怎么通信?
      答:什么是线程通信:不同线程之间传递数据,一般线程传递到主线程。 iOS中开启多线程的方式:三种。比如:在子线程下载图片,然后回到主线程显示图片。
    3. 简述多线程的作用以及什么地方会用到多线程?OC实现多线程的方法有哪些?谈谈多线程安全问题的几种解决方案?何为线程同步,如何实现的?分线程回调主线程方法是什么,有什么作用?
      答:1>耗时操作、界面卡死的时候使用多线程
      2.>作用:可以同时执行多个任务,适当提高程序的执行效率。为了提高CPU的使用率,采用多线程的方式去同时完成几件事互不干扰。
      3>iOS中多线程的方法:NSThread、NSOperation、GCD、pthread
      4>使用场景:同时上传和下载多个文件:加载网络数据同时展示Loading的UI、大量数据I/O操作。
      5>资源共享造成的安全问题:多线程环境下,当多个线程同时操作共享资源的setter和getter方法时,会造成数据的读写错乱就是线程安全问题。
      6>线程同步技术:使多个线程一次有序的执行,实现方案是加锁,把共享资源的读写操作锁起来常用的是互斥锁。
      7>线程间的通信:一个线程执行完任务之后,把执行的结果传递到另外一个线程叫线程间通信。线程间通信可用来在两个线程间传递数据。

    相关文章

      网友评论

      • 401efce826f5:非常感谢!
      • 865020e67958:不错
        MiracleGl:@简至之极 感谢:kissing_heart:
      • kalen5241:读懂就是好文章
        MiracleGl:@kalen5241 :blush:
      • aebc0aeeb1af:一看作者就是个高手,好几年经验了吧!
        MiracleGl:嘿嘿:kissing_heart:
      • 纠结的哈士奇:插个队,问一下,楼主,你简书的代码怎么弄的?没看到插入代码块啊

        求教~:smile:
        MiracleGl:http://www.jianshu.com/p/65b6c27a2179 这有我写的一个Markdown的使用 应该能解决你的问题:yum:
      • 幸福的李雨龙:函数名很赞:zanting
        MiracleGl:不要在意那些细节:joy:
      • 景丰:己阅,很好!
        MiracleGl:@景丰 感谢
      • 小苗晓雪:有个问题请教作者!主队列也是一个串型队列,请问在主队列里放同步任务会死锁!为什么串型里放同步任务不死锁?!主队列是一个特殊的串型队列它特殊在哪儿?!
        乔克_叔叔:主队列放同步任务会产生死锁是因为程序默认就是运行在主线程也就是主队列,当运行到同步任务时,dispatch_sync 阻塞调用线程(主线程),等待block()执行结束才能继续执行。此时 你已经在主线程中 调用 dispatch_sync, 主线程 需要等待 dispatch_sync中的任务执行完才能继续执行 ,导致相互等待对方执行。而你说的串型里放同步任务不死锁是错误的,如下:
        - (void)test1 {
        dispatch_queue_t queue = dispatch_queue_create("queue",DISPATCH_QUEUE_SERIAL);
        dispatch_sync(queue, ^{
        //同步任务1
        [self test2:queue];
        NSLog(@"haha");
        });
        }

        - (void) test2:(dispatch_queue_t)queue {
        dispatch_sync(queue, ^{
        //同步任务2
        NSLog(@"xixi");
        });
        }

        如果你在同步任务1中再执行同步任务2,串行队列queue 中先后添加了同步任务1,同步任务2,当执行到同步任务2时它需要等待之前的同步任务1执行完才能执行,而同步任务1也需要等待同步任务2执行完才能继续执行,从而导致了死锁,这跟主队列放同步任务会产生死锁是类似的。而主队列特殊性是主队列是系统默认创建好了的

      本文标题:关于iOS多线程

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