美文网首页
多线程第二弹 - 常用的GCD函数

多线程第二弹 - 常用的GCD函数

作者: Alexander | 来源:发表于2016-03-22 21:05 被阅读66次

    前言

    前面第一章知识简单介绍了多线程的理论知识,并没有太多的实际示例,那么本章就主要介绍关于GCD相关的函数,通过每个示例,更加深刻的了解多线程.如果文中观点有错,请大家提出来,相互进步

    • 队列的创建
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        /*
         const char *label : 队列的标签
         dispatch_queue_attr_t attr : 队列的类型(串行还是并发)
         #define DISPATCH_QUEUE_SERIAL 或 NULL :都表示串行
         #define DISPATCH_QUEUE_CONCURRENT
         */
        dispatch_queue_t queue = dispatch_queue_create("com.William.Alex", DISPATCH_QUEUE_CONCURRENT);
    
        dispatch_async(queue, ^{
    
            // 执行的任务
        });
    }
    
    • 常用方法
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 获取当前线程
        [NSThread currentThread];
    
        // 获取主队列
        NSLog(@"%@",dispatch_get_main_queue());
    
        // 获取全局并发队列
        /*
         long identifier : 优先级(0:表示默认优先级)
         unsigned long flags : 预留字段,填0即可
         */
        NSLog(@"%@",dispatch_get_global_queue(0, 0));
    }
    
    

    延迟操作
    上一章我们大概提了一下延迟操作的三种方法,但是没有细说,在实际开发中,很多时候都需要使用到延迟操作,比如说使用GCD的延迟操作来模拟网络延迟,从而找出隐藏较深的Bug.本章主要讲GCD的延迟操作.

    • 方法


      GCD的延迟操作.png
    • GCD中的延迟操作函数
    // 参数 1 :时间  参数 2 : 队列  参数3 : 任务block代码块
    dispatch_after(dispatch_time_t when, dispatch_queue_t queue, ^(void)block);
    
    //  参数 : delayInSeconds : 延迟的时间,表示多少时间后执行block代码块中的代码
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 需要执行的任务
        });
    
    

    dispatch_after示例

    
    // 示例虽然简单,但是通俗易懂
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
    
        NSLog(@"点击屏幕就会立即打印出来");
    
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
            NSLog(@"点击屏幕后,2秒之后才会答应出来,不信自己去试试");
        });
    
    }
    
    

    补充 : performSelector方法虽然不是很常用,但是最好也要知道有这个方法可以实现延迟操作

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        NSLog(@"仍然需要有一个参照,不然谁知道是不是延迟了3秒");
    
        [self performSelector:@selector(run) withObject:nil afterDelay:3.0];
    }
    
    - (void)run {
    
        NSLog(@"看来真的延迟了3秒后才打印");
    }
    
    • 控制线程的状态
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 创建一条线程
        NSThread *thread = [[NSThread alloc] initWithTarget:self
                                                  selector:@selector(run)
                                                    object:nil];
    
        // 开启线程(当线程执行完毕之后,自动进入死亡状态)
        [thread start];
    
        // 手动阻塞线程
        [NSThread sleepForTimeInterval:0.2];
    
        // 强制停止线程(一旦线程停止(死亡)状态,就无法再次开启该线程)
        [NSThread exit];
        }
    
    - (void)run {
    
        NSLog(@"跑你妹啊");
    }
    

    线程的安全隐患问题

    • 多线程的安全隐患 : 为什么多线程存在安全隐患等问题呢,具体是什么原因造成的呢?带着这些问题我们一步一步的学习为什么多线存在安全隐患,解决方法是怎样的.

    经典示例 : 资源共享

    • 分析 : 当多个线程访问同一块资源时,就很容易引发数据混乱和数据安全等问题.总之,多个线程访问同一个对象,同一个变量,或者同一个文件时,就可能发生数据紊乱等现象.
    • 解决方案 : 针对上面所述的问题,这里引入了一个新的概念:互斥锁.

    互斥锁

    • 互斥锁的原理 : 它的原理就是线程同步技术.
    • 线程同步 : 即多条线程在同一条线生执行任务(并且执行任务是有序的,是按顺序执行任务的).
    • 互斥锁的使用格式 : @synchronized(锁对象){ // 需要锁定的代码 }

    互斥锁的优缺点

    • 优点
      • 能够有效防止因多条线程因抢夺资源而引发的数据安全问题
    • 缺点
      • 需要消耗大量的CPU资源

    互斥锁的注意点

    • 锁定一份代码只能使用1把锁,不管你嵌套多少把锁都是无效的.
    • 互斥锁的使用前提 : 多条线程抢夺同一块资源.

    互斥锁的应用场景

    • 讲到互斥锁的应用场景,这里我们又要引入每天都能接触到两个概念:即原子和非原子属性,对,就是我们每天定义属性时,传入的修饰参数.

    • OC中定义属性时有两种选择:即nonatomic和atomic

    • nonatomic : 非原子属性,不会为setter方法加互斥锁

    • atomic : 原子属性,会为setter方法加锁(系统默认是atomic是需要枷锁的).

    • 知识补充 : 一般我们开发APP时,都是使用nonatomic,就性能而言非原子属性的性能高于原子属性,原因是atomic是线程安全的,加锁会消耗大量的CPU资源,nonatomic虽然是非线程安全,但是对于内存较小的移动设备,尽量使用nonatomic.

    示例

    (忽略多次点击程序会崩溃,这里主要想演示线程加锁)

    #import "ViewController.h"
    
    @interface ViewController ()
    
    /** 火车票数量 */
    @property (nonatomic, assign) NSInteger titketCount;
    
    /** 售票员1 */
    @property(nonatomic, strong) NSThread *conductor1;
    /** 售票员2 */
    @property(nonatomic, strong) NSThread *conductor2;
    /** 售票员3 */
    @property(nonatomic, strong) NSThread *conductor3;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        // 假设有50张火车票
        self.titketCount = 50;
    
        // 创建三个售票窗口
        self.conductor1 = [[NSThread alloc] initWithTarget:self
                                                       selector:@selector(saleTitkets)
                                                         object:nil];
        self.conductor1.name = @"售票员1";
    
        self.conductor2 = [[NSThread alloc] initWithTarget:self
                                                       selector:@selector(saleTitkets)
                                                         object:nil];
        self.conductor2.name = @"售票员2";
    
        self.conductor3 = [[NSThread alloc] initWithTarget:self
                                                       selector:@selector(saleTitkets)
                                                         object:nil];
        self.conductor3.name = @"售票员3";
    }
    
    #pragma mark - 开启线程
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
    
        [self.conductor1 start];
        [self.conductor2 start];
        [self.conductor3 start];
    }
    
    #pragma mark - 开始售票
    
    - (void)saleTitkets {
    
        while (1) {
        @synchronized(self) {
                NSInteger count = self.titketCount;
                if (count > 0) {
                    self.titketCount = count - 1;
    
                    NSLog(@"%@卖了一张火车票,还剩%ld张火车票",[NSThread currentThread].name, self.titketCount);
                } else
                {
                    NSLog(@"没票了");
                    break;
                }
            }
        }
    }
    

    打印结果(没有枷锁时的部分打印)

    2016-03-22 14:57:28.770 01 - 多线程[1241:105971] 售票员3卖了一张火车票,还剩47张火车票
    2016-03-22 14:57:28.770 01 - 多线程[1241:105969] 售票员1卖了一张火车票,还剩49张火车票
    2016-03-22 14:57:28.770 01 - 多线程[1241:105970] 售票员2卖了一张火车票,还剩48张火车票
    2016-03-22 14:57:28.771 01 - 多线程[1241:105971] 售票员3卖了一张火车票,还剩46张火车票
    2016-03-22 14:57:28.771 01 - 多线程[1241:105969] 售票员1卖了一张火车票,还剩45张火车票
    2016-03-22 14:57:28.771 01 - 多线程[1241:105970] 售票员2卖了一张火车票,还剩44张火
    
    • 根据上面的打印,我们没有添加互斥锁,造成的结果就是数据紊乱,第49对应的火车票都还没有被售出去,就直接将第47对应火车票售出.应该是先售出第49对应的火车票,才能去售卖第47对应的火车票.

    打印结果(加锁后的部分打印)

    2016-03-22 15:02:55.688 01 - 多线程[1268:108069] 售票员1卖了一张火车票,还剩49张火车票
    2016-03-22 15:02:55.689 01 - 多线程[1268:108070] 售票员2卖了一张火车票,还剩48张火车票
    2016-03-22 15:02:55.689 01 - 多线程[1268:108071] 售票员3卖了一张火车票,还剩47张火车票
    2016-03-22 15:02:55.689 01 - 多线程[1268:108069] 售票员1卖了一张火车票,还剩46张火车票
    2016-03-22 15:02:55.689 01 - 多线程[1268:108070] 售票员2卖了一张火车票,还剩45张火车票
    2016-03-22 15:02:55.690 01 - 多线程[1268:108071] 售票员3卖了一张火车票,还剩44张火车票
    2016-03-22 15:02:55.690 01 - 多线程[1268:108069] 售票员1卖了一张火车票,还剩43张火
    
    
    • 注意 : 使用@synchronized(锁对象) { 需要锁定的代码}时,注意它的位置,我又一次将循环也放在锁内,导致打印的结果:火车票全是"售票员1"卖出的.这明显不对.我们只需要将售票环节锁住即可.

    一次性代码(单例:下一章专门讲单例模式)

    • 一次性代码 : 使用dispatch_once_t函数,能保证在整个应用程序在运行过程中只会被执行一次,其内部的代码系统默认是线程安全的.
    - (void)once {
    
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSLog(@"内部的代码系统默认是线程安全的");
            NSLog(@"整个进程运行过程中,只会有一份内存");
        });
    
    }
    
    • 一次性代码的应用场景 : 单例模式,具体下一章会专门介绍单例模式的原理以及使用,这里就不再过多介绍了.

    快速迭代遍历

    • 快速迭代类似于for循环,但是它在并发队列中时,会并发执行block中的任务,所以它的效率肯定是比一般的for循环要更加高效一点
    • 注意最后的打印,dispatch_apply是会阻塞主线程的。这个NSLog打印会在dispatch_apply迭代结束后才开始执行,如果没有打印,那么说明阻塞了.
    • dispatch_apply快速迭代会避免线程爆炸,因为GCD会管理线程

    快速剪切文件-dispatch_apply示例

    - (void)apply {
    /*
    思路 :
    1, 获取文件所在的原始文件夹路径
    2, 获取文件要去的新文件夹的路径
    3, 创建一个文件管理者
    4, 拿到原始路径下的所有文件
    5, 遍历原始路径下的的文件夹中的所有文件
    6, 拼接原始路径和新路径为全路径
    7, 使用异步 + 并发队列 将会话管理者中的所有文件从原始文件移动到新文件夹下.
    */
        // 创建一个全局队列
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
        // 设置剪切文件的路径
        NSString *from = @"/Users/mac/Desktop/Frome";
        NSString *to = @"/Users/mac/Desktop/To";
    
        /*
         NSFileManager也是一个单例
         创建文件管理者
         */
        NSFileManager *fileManager = [NSFileManager defaultManager];
    
        // 拿到from文件路径中所有子路径
        NSArray *subPaths = [fileManager subpathsAtPath:from];
    
        /*
         参数 1: from路径下的所有子路径的总数. 参数 2 : 队列  参数 3: block代码块,双击后需要添加索引index
         */
        dispatch_apply(subPaths.count, queue, ^(size_t index) {
    
            // 根据索引拿到需要剪切的文件
            NSString *subpath = subPaths[index];
    
            // 拼接全路径
            NSString *fullFromPath = [from stringByAppendingPathComponent:subpath];
            NSString *fullToPath = [to stringByAppendingPathComponent:subpath];
    
            /*
             剪切文件:从from文件夹中剪切文件到to文件夹
             */
            [fileManager moveItemAtPath:fullFromPath toPath:fullToPath error:nil];
        });
    }
    

    队列组 : dispatch_group

    • dispatch_group的作用 : dispatch_group是专门用来监听多个异步任务,换句话说,dispatch_group实例是用于追踪不同队列中不同的任务.
    • 当dispatch_group(队列组)中所有的事件都执行完毕后,GCD提供了两种API方式发送通知.
      • 第一种方式 : dispatch_group_wait
        • 会阻塞线程,等所有任务都完成或者等待超时时才会执行下一个任务.
      • 第二种方式 : dispatch_group_notify(重点讲这个)
        -不会阻塞线程,是异步执行block中的任务.

    示例

    要求分别下载两张网络上的图片,然后将两张图片合成一张显示出来

    • 分析 : 上述示例简单总结为:
      • 1, 分别执行两个耗时的异步操作.
      • 2, 等2个耗时的异步操作执行完毕之后,再回到主线程刷新UI界面
    #import "ViewController.h"
    
    @interface ViewController ()
    
    /**
     *  显示下载的图片
     */
    @property (weak, nonatomic) IBOutlet UIImageView *AppleImage;
    
    /**
     *  图片 1
     */
    @property (nonatomic, weak) UIImage *image1;
    
    /**
     *  图片 2
     */
    @property (nonatomic, weak) UIImage *image2;
    
    
    @end
    
    @implementation ViewController
    
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    
        [self group];
    }
    
    - (void)group {
    
        // 创建全局队列
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        // 创建一个队列组
        dispatch_group_t groupQueue = dispatch_group_create();
    
        // 下载第一张图片
        dispatch_group_async(groupQueue, queue, ^{
    
            // 根据url下载图片
            NSURL *url = [NSURL URLWithString:@"网络图片Ur"];
    
            // 将图片以二进制的形式保存到本地
            NSData *imageData = [NSData dataWithContentsOfURL:url];
    
            // 显示图片
            self.image1 = [UIImage imageWithData:imageData];
        });
    
    
        // 下载第二种图片
        dispatch_group_async(groupQueue, queue, ^{
    
            // 通过url找到图片
            NSURL *url = [NSURL URLWithString:@"网络图片Url"];
    
            // 将图片保存到本地
            NSData *imageData = [NSData dataWithContentsOfURL:url];
    
            // 显示图片
            self.image2 = [UIImage imageWithData:imageData];
        });
    
    
        // 执行完毕耗时操作,将图片合二为一,然后刷新界面显示图片
        dispatch_group_notify(groupQueue, queue, ^{
    
            // 开启上下文
            UIGraphicsBeginImageContext(CGSizeMake(100, 100));
    
            // 绘制图片
            [self.image1 drawInRect:CGRectMake(0, 0, 100, 100)];
            [self.image2 drawInRect:CGRectMake(50, 0, 100, 100)];
    
            // 获取合成的图片
           UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    
           // 关闭上下文
            UIGraphicsEndImageContext();
    
            // 回到主线程刷新界面,显示图片
            dispatch_async(dispatch_get_main_queue(), ^{
    
                // 显示图片
                self.AppleImage.image = image;
            });
        });
    }
    
    @end
    
    
    • 总结 : 这里使用了图形上下文的知识,因为是需要将下载完毕的两张图片合成一张新图片展示到屏幕上.但是注意,当队列中耗时的操作执行完毕之后,必须要调用_notify这个方法,在这个方法中合成新图片,当该方法中的队列中的任务完成之后,才会来到主线程,进行刷新UI界面.处理UI事件.

    线程间的通信

    在应用程序运行过程中,线程之间并不是孤立存在的,它们之间是可以线程通讯的

    • 线程通信的具体体现

      • 一条线程想要传递数据给另一条线程
      • 当在某条线程上执行完毕任务之后,需要跳转到另一条线程上去执行任务
    • 比如上面队列组中的示例(下载图片),当程序开始下载图片是,原理是:会开启子线程执行下载图片的操作,当下载操作执行完毕之后,需要跳转到主线程上执行UI刷新操作.

    下载图片的线程原理

    线程间的通信.png

    线程之间通信的三种方法

    // 跳转到指定线程上执行任务(或者将数据传递到指定线程)
    [self performSelector:(nonnull SEL) onThread:(nonnull NSThread *) withObject:(nullable id) waitUntilDone:(BOOL)];
    [self performSelector:(nonnull SEL) onThread:(nonnull NSThread *) withObject:<#(nullable id)#> waitUntilDone:(BOOL) modes:(nullable NSArray<NSString *> *)];
    
    // 跳转到主线程上执行任务(或者将数据传递到主线程)
    [self performSelectorOnMainThread:(nonnull SEL) withObject:(nullable id) waitUntilDone:(BOOL)]
    [self performSelectorOnMainThread:(nonnull SEL) withObject:(nullable id) waitUntilDone:(BOOL) modes:(nullable NSArray<NSString *> *)]
    
    
    • 注意 :他们虽然传入的参数不一样,但是功能是一样的.

    下面介绍一种比较常用的

    
        // 创建全局的并发队列
        dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        dispatch_async(globalQueue, ^{
    
            // 执行子线程上耗时的操作
    
            dispatch_async(dispatch_get_main_queue(), ^{
    
                // 回到主线程,执行UI刷新的操作
            });
    
        });
    
    • 这种方式的线程通信: 以将它的原理看出一个嵌套.在子线程中嵌套一个主线程,比如:下载图片

    • 首先在获取一个异步的并发队列,在并发队列中执行下载图片(耗时的操作),然后在并发队列中嵌套一个异步的队列,该队列是主线程,回到主线程处理UI更新操作.

    相关文章

      网友评论

          本文标题:多线程第二弹 - 常用的GCD函数

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