多线程第一弹 - GCD

作者: Alexander | 来源:发表于2016-03-22 11:32 被阅读107次

    前言

    在开发过程中,我们会经常和网络打交道,与网络相关的知识无疑是iOS开发中的难点之一,学习多线程,我走了很多弯路,遇到过很多坑,不过对于我们来说,学习就是需要从一个坑爬出来,然后又跳到另一个坑.本篇文章主要介绍多线程相关的基本知识,想要深入了解,可以关注CocoaChina微信公众号,里面有很多大牛的技术博客.感谢@小笨狼Lc提供的相关资料.接下来我们正式开始我们的多线程之旅吧.

    在正式接触多线程的核心知识前,我们需要了解一些与网络相关的基本知识.比如说:进程和线程分别代表的是什么意思. 两者有什么联系等.

    进程与线程的基本概念

    • 什么是进程 : 所谓的进程指的就是系统在运行过程中正在运行的一个应用程序.
    • 进程的特点 : iOS系统中进程之间是相互独立的,也就是说,每个进程都是在其受保护的内存空间中运行的.这就是为什么iOS系统用起来比某些系统(没有特指)更加的流畅的原因.下面我们根据一副图来说明iOS中进程的特点.
      在我们的Mac电脑上同时打开快播和迅雷两款软件,从图片中可以看出,他们之间是没有联系的,两两之间是相互独立的.我们可以通过Mac电脑上的"活动监视器"来查看所有正在开启的进程
    进程.png
    • 什么是线程 :一个进程(后面我们直接就说成应用程序)想要执行任务,那么它必须要有线程,至少要有一条线程,原因是应用程序中的所有的任务都是在线程上执行的,也就是说,没有线程就不能执行任务.

    • 比如根据下面的图片,可以看出当我们开启QQ音乐和快播两款软件时需要一条对应的线程,并且如果我们想要播放音乐或者是下载电影(0),那么他们也要有一条与之对应的线程.总的来说,只要应用程序想要执行某项任务,那么它就必须要有线程,至少要有一条.注意:播放音乐的线程和下载电影的线程之间是相互独立的.

    线程.png

    线程的串行与并行

    • 线程串行 : 在同一条线程上执行所有的任务,这种运行方式就叫做串行.
    • 串行的特点 : 如果一个进程执行任务的方式是串行执行,那么就表示该进程上的所有任务都是在同一条线程上执行的,并且他们的执行方式是按顺序执行的,也就是说,在同时间,一条线程只能执行一个任务,只有当当前任务执行完毕之后,才会去继续执行下一个任务(执行的任务是有序的).
    • 比如下面的图片:只有一条线程(黑色箭头).我们需要下载A, B, C三个文件,这时候的执行顺序是,只有当"A文件下载"完毕之后,才会去执行"下载文件B"操作,最后当"下载文件B"执行完毕之后,才去执行"下载文件C".
    串行执行.png
    • 什么是多线程 : 在同一个进程中,拥有多条线程,并行执行多个任务(关于多线程,下面会有更加详细的介绍,这里只是其抛砖引玉的作用).
    • 什么是并行 : 多条线程同时执行多个任务(执行的任务是无序的)比如说下面的图片中,开启了三条线程(黑色箭头)分别执行下载对应的文件,就是说:在同一个进程中开启3条线程,同时执行下载操作,实现ABC三个文件同时下载.
    并行执行.png
    • 注意 : 并行执行的本质并不是真正意义上的同时执行,那只是给用户视觉上的错觉.它的实质是在同一时间刻也是只能执行一个任务.它只是线程之间高速切换,给人一种同时下载资源的错觉.(知道就可以了,不必较真儿)

    多线程

    • 什么是多线程 : 在一个进程中可以开启多条线程,同时执行多个任务.
    • 使用多线程的优缺点
    • 1, 多线程的优点
      • 1.1, 在一定程度上可以提高程序的执行效率
      • 1.2, 能够适当提高资源的利用率(比如说:CPU, 内存等利用率)
    • 2, 多线程的缺点
      • 2.1, 创建线程是有一定的开销的,如果开启大量的线程的话,肯定会降低程序的性能.
      • 2.2, 线程开启的越多,CPU在调度线程上的开销就越大
      • 2.3, 程序的设计变得更加复杂,比如线程间的通信(下面会详细介绍),又或者多线程的数据共享

    iOS中多线程的实现方案

    • pthread : 一套通用的多线程并且基于C语言的API,由程序员来管理线程的生命周期,使用难度很大(本篇文章就不介绍了)
    • NSThread : 相对于pthread,它更加面向对象,使用起来相对简单,生命周期由程序员来管理,但是在开发中一般不用,使用场景比如说:我想要知道当前线程是主线程还是子线程([NSThread currentThread])
    • GCD : 是对NSThread进一步的封装,使用起来更加建档方便快捷,主要是它使用了block代码块.是一套基于C的API,生命周期由系统自动管理.
    • NSOperation : 是对GCD进一步的封装,底层是GCD,相对GCD来说,使用更加面向对象,线程的命周期也是由系统自动管理.

    GCD

    什么是GCD : GCD是Grand Central Disparch的简称,GCD可以自动管理线程的生命周期(创建线程, 调度线程以及销毁线程).所以我们不需要再去关注线程,只需要高数GCD执行的是什么任务以及以什么方式执行任务即可.

    GCD的两个核心概念(任务和队列)

    • 任务: 所谓的任务就是我们想要执行的代码,换句话说就是block代码块中的代码又或者是一个指针函数.
    • 队列 : 可以将队列比喻成存放任务的容器

    GCD的两个核心步骤:

    • 制定任务 : 即想要实现什么效果的代码
    • 添加任务 : 即将确定的任务添加到队列中,GCD会自动将添加的任务取出来,放到对应线程上去执行.这里涉及到任务进出一个原则:FIFO(先进先出)
    先进先出原则.png
    • dispatch_queue_t queue
      对象就有内存。跟普通OC对象类似,我们可以用dispatch_retain()和dispatch_release()对其进行内存管理,当一个任务加入到一个queue中的时候,任务会retain这个queue,直到任务执行完成才会release。
      在iOS 6之后,dispatch对象已经支持ARC,所以在ARC工程之下,我们可以不用担心他的内存是否被释放.

    • 队列的知识补充:要申明一个dispatch的属性。一般情况下我们只需要用strong即可。

    @property (nonatomic, strong) dispatch_queue_t queue;
    
    • 注意:如果你是写一个framework,framework的使用者的SDK有可能还是古董级的iOS6之前。那么你需要根据OS_OBJECT_USE_OBJC做一个判断是使用strong还是assign。
    #if OS_OBJECT_USE_OBJC
    @property (nonatomic, strong) dispatch_queue_t queue;
    #else
    @property (nonatomic, assign) dispatch_queue_t queue;
    #endif
    

    GCD中的同步函数与异步函数

    异步函数(async) : 可以在新线程上执行任务具备开启新线程的能力.
    // 参数传的是一个block
    dispatch_async(dispatch_queue_t queue, ^(void)block);
    
    // 参数传的是一个指针函数
    dispatch_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
    
    • 两个异步执行的API实现的功能是一样的,都是讲任务提交到指定的队列中,提交后就立即返回,不用等待任务的执行完毕. 提交之后系统内部会对队列queue进行一次retain操作,等到任务执行完毕之后,队列queue再被release一次.它们之间唯一的区别是最后一个参数不同,前者是接收block为参数,后者是接收函数为参数.

    • **注意(重点:面试可能会被问到) :多线程方法的block中为什么能使用self,不会造成循环引用吗?
      **

    • 答: 首先肯定是不会有循环引用的,用不着(__weak typeof(self) weakSelf = self).原因:是当使用ispatch_async的时候block会被copy(深拷贝)一次,虽然当block执行任务完成之后block会被release,但是此时的系统已经拥有了之前拷贝的block了,所以用不着担心循环引用问题,也就是说,block中的self不用将之弱化,直接使用即可(注意:这里只是特例,如果没有copy那么还是需要将block里面的self变为weakSelf)

    • 异步是一个比较抽象的概念,简单的说就是将任务加入到队列中之后,立即返回,不需要等待任务的执行.我们用代码加深一下对概念的理解

    异步函数示例
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSLog(@"异步函数之前 %@",[NSThread currentThread]);
        /**
         * 参数 1: 队列的名称  参数 2: 队列的类型
         * DISPATCH_QUEUE_SERIAL NULL  // 表示串行队列
         * DISPATCH_QUEUE_CONCURRENT  // 表示并发队列
         */
        dispatch_queue_t queue = dispatch_queue_create("我的微博是@WilliamAlex大叔", DISPATCH_QUEUE_SERIAL);
        dispatch_async(queue, ^{
    
            NSLog(@"异步函数中%@",[NSThread currentThread]);
        });
    
        NSLog(@"异步函数之后 %@",[NSThread currentThread]);
    }
    
    打印结果
    2016-03-21 21:31:17.349 01 - 多线程[998:85748] 异步函数之前 <NSThread: 0x7fe800707430>{number = 1, name = main}
    2016-03-21 21:31:17.350 01 - 多线程[998:85748] 异步函数之后 <NSThread: 0x7fe800707430>{number = 1, name = main}
    2016-03-21 21:31:17.350 01 - 多线程[998:85848] 异步函数中<NSThread: 0x7fe80077c100>{number = 2, name = (null)}
    
    • 结论 : 当使用异步函数时,它只是将任务放置在队列中而已,并不需要等待任务的执行,当将任务提交到队列中时,就会立即返回,根据打印结果可以看出异步函数中的打印是最后才打印的.也就是说,使用异步函数是不会造成线程死锁等问题.
    同步函数 : 只能在当前线程上执行任务,不具备开启子线程的能力.
    // 参数是block
    dispatch_sync(dispatch_queue_t queue, ^(void)block);
    
    // 参数是函数
    dispatch_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
    
    
    • 两个方法的作用完全是相同的,都是讲任务提交到queue中,任务添加到队列中后,不会立即返回,必须要等待任务执行结束之后再返回,他们的唯一的区别就是最后一个接收的参数不一样,一个是接收block代码块,一个接收的是指针函数.

    • 同步表示任务加入到队列中之后不会立即返回,等待任务完成再返回。语言的描述比较抽象,我们再次用代码加深一下对概念的理解

    同步函数示例
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSLog(@"同步函数之前 %@",[NSThread currentThread]);
        /**
         * 参数 1: 队列的名称  参数 2: 队列的类型
         * DISPATCH_QUEUE_SERIAL NULL  // 表示串行队列
         * DISPATCH_QUEUE_CONCURRENT  // 表示并发队列
         */
        dispatch_queue_t queue = dispatch_queue_create("我的微博是@WilliamAlex大叔", DISPATCH_QUEUE_SERIAL);
    
        dispatch_sync(queue, ^{
    
            NSLog(@"同步函数中%@",[NSThread currentThread]);
        });
    
        NSLog(@"同步函数之后 %@",[NSThread currentThread]);
    }
    
    打印结果
    2016-03-21 22:23:28.534 01 - 多线程[1037:95068] 同步函数之前 <NSThread: 0x7f8fd2601920>{number = 1, name = main}
    2016-03-21 22:23:28.535 01 - 多线程[1037:95068] 同步函数中<NSThread: 0x7f8fd2601920>{number = 1, name = main}
    2016-03-21 22:23:28.535 01 - 多线程[1037:95068] 同步函数之后 <NSThread: 0x7f8fd2601920>{number = 1, name = main}
    
    
    • 总结 : 根据打印结果可以看出,它是按顺序执行的,当同步函数将任务添加到队列中后,不会立即返回,而是等待任务执行完毕之后才返回的,而且全部都是在主线程中执行的任务.

    • 先在main queue中执行第一个NSLog

    • dispatch_sync会将block提交到queue中,等待block的执行

    • queue中block前面的任务执行完成之后,block执行

    • block执行完成之后,dispatch_sync返回

    • dispatch_sync之后的代码执行

    线程死锁问题

    • 造成死锁的原因 : 由于dispatch_sync(同步)需要等待block被执行,这就非常容易发生死锁。如果一个串行队列,使用dispatch_sync提交block到自己队列中,就会发生死锁
    死锁示例
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSLog(@"同步函数之前 %@",[NSThread currentThread]);
        /**
         * 参数 1: 队列的名称  参数 2: 队列的类型
         * DISPATCH_QUEUE_SERIAL NULL  // 表示串行队列
         * DISPATCH_QUEUE_CONCURRENT  // 表示并发队列
         */
        dispatch_queue_t queue = dispatch_queue_create("我的微博是@WilliamAlex大叔", DISPATCH_QUEUE_SERIAL);
    
            dispatch_async(queue, ^{
                NSLog(@"异步函数中%@",[NSThread currentThread]);
                dispatch_sync(queue, ^{
    
                    NSLog(@"同步函数中%@",[NSThread currentThread]);
                });
            });
    
            NSLog(@"同步函数之后 %@",[NSThread currentThread]);
    }
    
    
    打印结果
    2016-03-21 22:40:33.280 01 - 多线程[1112:100793] 同步函数之前 <NSThread: 0x7ff17a707510>{number = 1, name = main}
    2016-03-21 22:40:33.281 01 - 多线程[1112:100793] 同步函数之后 <NSThread: 0x7ff17a707510>{number = 1, name = main}
    2016-03-21 22:40:33.281 01 - 多线程[1112:100849] 异步函数中<NSThread: 0x7ff17a501380>{number = 2, name = (null)}
    
    
    • 总结 :
      • 上面的代码已经造成了死锁现象,原因是同步(dispatch_sync)将任务添加到队列中后,仍然需要等待任务执行完毕,前面创建队列queue时,它的类型是串行队列,串行队列的本质是一个接一个的执行,所以block执行任务也需要等待前面的任务执行完毕,也就是等待dispatch_sync执行完毕,两者相互谦让,相互等待,导致两个都没有执行,所以执行永远没有执行完毕,这就是造成死锁的本质原因.
    • 根据上面打印的结果可以得出造成死锁的条件:
      • 创建的队列是串行队列
      • 使用同步(dispatch_sync)将任务添加到自己的队列中
    • 注意 : 如果queue是并行队列,或者将任务加入到其他队列中,就不会发生死锁的现象了。
    写到这里我们需要总结一下,否则以后我们只会越来越害怕多线程,害怕去接触网络相关的知识,做事情不能模棱两可,既然做了,就要弄明白.
    • 上面我们学习了多线程执行任务的方法(即同步和异步)和队列的运行方式(串行和并行)这里会很绕,需要弄清楚他们之间的本质,才能更好的运用它们.
    • 下面主要是通过它们之间的区别以及其自身本质来区分他们
    • 同步和异步的主要区别: 是否具备开启子线程的能力
      • 同步 : 只能在当前线程上执行任务,不具备开启新线程的能力
      • 异步 : 可以在新线程上执行任务,具备开启新线程的能力
    • 串行和并发的主要区别: 任务的执行方式
      • 串行 : 只要当当前任务执行完毕之后,才能执行下一个任务,是有序的执行任务
      • 并发 : 允许多个任务(并发)同时执行
        同步:异步:串行:并发.png

    下面我们具体举例

    • 同步函数 + 主队列(死锁啦)
    - (void)viewDidLoad {
        [super viewDidLoad];
        NSLog(@"-------------begin");
        // 1, 获取主队列
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //    dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
    
        // 将任务添加到队列中
        dispatch_sync(mainQueue, ^{
    
            NSLog(@"1我的微博@WilliamAlex大叔%@",[NSThread currentThread]);
    
        });
    
        NSLog(@"-------------End");
    }
    }
    

    打印结果

    2016-03-22 10:39:17.524 01 - 多线程[655:22430] -------------begin
    
    
    • 注意 :示例中的全局队列globalQueue是用来检测线程是不是死锁了.

    异步函数 + 主队列

    - (void)viewDidLoad {
        [super viewDidLoad];
        NSLog(@"-------------begin");
        // 1, 获取主队列
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
        // 将任务添加到队列中
        dispatch_async(mainQueue, ^{
    
            NSLog(@"哎呀!妈呀, 多线程的水很深啊%@",[NSThread currentThread]);
        });
    
        NSLog(@"-------------End");
    }
    

    打印结果

    2016-03-22 10:44:01.294 01 - 多线程[668:23853] -------------begin
    2016-03-22 10:44:01.294 01 - 多线程[668:23853] -------------End
    2016-03-22 10:44:01.302 01 - 多线程[668:23853] 哎呀!妈呀, 多线程的水很深啊<NSThread: 0x7fe583700bd0>{number = 1, name = main}
    
    
    • 注意 : 打印结果表示 : 当使用异步函数时,当将block中的任务添加到队列中后,就会立即返回,不会等待任务的执行.

    同步函数 + 串行队列

    - (void)viewDidLoad {
        [super viewDidLoad];
        NSLog(@"-------------begin");
        // 1, 获取串行:DISPATCH_QUEUE_SERIAL 或 NULL即可
        dispatch_queue_t syncSerial = dispatch_queue_create("WilliamAlex大叔", DISPATCH_QUEUE_SERIAL);
    
        // 将任务添加到队列中
        dispatch_sync(syncSerial, ^{
    
            NSLog(@"1WilliiamAlex大叔%@",[NSThread currentThread]);
        });
    
        dispatch_sync(syncSerial, ^{
    
            NSLog(@"2WilliiamAlex大叔%@",[NSThread currentThread]);
        });
    
        dispatch_sync(syncSerial, ^{
    
            NSLog(@"3WilliiamAlex大叔%@",[NSThread currentThread]);
        });
        NSLog(@"-------------End");
    }
    

    打印结果

    2016-03-22 10:52:32.478 01 - 多线程[703:27166] -------------begin
    2016-03-22 10:52:32.478 01 - 多线程[703:27166] 1WilliiamAlex大叔<NSThread: 0x7fbb0b6075d0>{number = 1, name = main}
    2016-03-22 10:52:32.479 01 - 多线程[703:27166] 2WilliiamAlex大叔<NSThread: 0x7fbb0b6075d0>{number = 1, name = main}
    2016-03-22 10:52:32.479 01 - 多线程[703:27166] 3WilliiamAlex大叔<NSThread: 0x7fbb0b6075d0>{number = 1, name = main}
    2016-03-22 10:52:32.479 01 - 多线程[703:27166] -------------End
    
    • 注意: 从打印结果中看,同步+串行:当将block中的任务添加到队列中后,并不会立即返回,而是等待任务执行完毕,并且任务是一个一个执行的,是有序的,不会开启新的线程.

    异步函数 + 串行队列

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSLog(@"-------------begin");
        // 1, 获取串行:DISPATCH_QUEUE_SERIAL 或 NULL即可
        dispatch_queue_t syncSerial = dispatch_queue_create("WilliamAlex大叔", DISPATCH_QUEUE_SERIAL);
    
        // 将任务添加到队列中
        dispatch_async(syncSerial, ^{
    
            NSLog(@"1WilliiamAlex大叔%@",[NSThread currentThread]);
        });
    
        dispatch_async(syncSerial, ^{
    
            NSLog(@"2WilliiamAlex大叔%@",[NSThread currentThread]);
        });
    
        dispatch_async(syncSerial, ^{
    
            NSLog(@"3WilliiamAlex大叔%@",[NSThread currentThread]);
        });
    
        NSLog(@"-------------End");
    }
    
    

    打印结果

    2016-03-22 10:57:45.433 01 - 多线程[716:28932] -------------begin
    2016-03-22 10:57:45.434 01 - 多线程[716:28932] -------------End
    2016-03-22 10:57:45.434 01 - 多线程[716:28983] 1WilliiamAlex大叔<NSThread: 0x7f7ff0c20060>{number = 2, name = (null)}
    2016-03-22 10:57:45.434 01 - 多线程[716:28983] 2WilliiamAlex大叔<NSThread: 0x7f7ff0c20060>{number = 2, name = (null)}
    2016-03-22 10:57:45.434 01 - 多线程[716:28983] 3WilliiamAlex大叔<NSThread: 0x7f7ff0c20060>{number = 2, name = (null)}
    
    
    • 注意 : 异步 + 串行 : 是有序执行的,开启了新的线程,注意,只要是异步函数,当将block中的惹怒添加到队列中后会立即返回,不会等待任务的执行.

    同步函数 + 并发队列

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSLog(@"-------------begin");
        // 1, 获取并发队列 : DISPATCH_QUEUE_CONCURRENT
    //    dispatch_queue_t syncSerial = dispatch_queue_create("WilliamAlex大叔", DISPATCH_QUEUE_CONCURRENT);
    
        // 可以直接使用全局的并发队列(第一个0:表示系统默认即可. 第二个0:表示预留字段)
        dispatch_queue_t globalQuque = dispatch_get_global_queue(0, 0);
    
        // 将任务添加到队列中
    
        dispatch_sync(globalQuque, ^{
    
            for (NSInteger i = 0; i < 3; i++) {
    
                NSLog(@"1WilliiamAlex大叔%@",[NSThread currentThread]);
            }
        });
    
        dispatch_sync(globalQuque, ^{
    
            for (NSInteger i = 0; i < 3; i++) {
    
                NSLog(@"2WilliiamAlex大叔%@",[NSThread currentThread]);
            }
        });
    
        NSLog(@"-------------End");
    }
    

    打印结果

    2016-03-22 11:13:23.988 01 - 多线程[794:35399] -------------begin
    2016-03-22 11:13:23.989 01 - 多线程[794:35399] 1WilliiamAlex大叔<NSThread: 0x7fafea506970>{number = 1, name = main}
    2016-03-22 11:13:23.989 01 - 多线程[794:35399] 1WilliiamAlex大叔<NSThread: 0x7fafea506970>{number = 1, name = main}
    2016-03-22 11:13:23.990 01 - 多线程[794:35399] 1WilliiamAlex大叔<NSThread: 0x7fafea506970>{number = 1, name = main}
    2016-03-22 11:13:23.990 01 - 多线程[794:35399] 2WilliiamAlex大叔<NSThread: 0x7fafea506970>{number = 1, name = main}
    2016-03-22 11:13:23.990 01 - 多线程[794:35399] 2WilliiamAlex大叔<NSThread: 0x7fafea506970>{number = 1, name = main}
    2016-03-22 11:13:23.990 01 - 多线程[794:35399] 2WilliiamAlex大叔<NSThread: 0x7fafea506970>{number = 1, name = main}
    2016-03-22 11:13:23.990 01 - 多线程[794:35399] -------------End
    
    • 注意 : 同步 + 并发:都是在主线程上执行的,并且执行方式是只有当前面的所有任务有序低执行完毕之后,才会执行下一个任务,不具备开启新线程的能力.

    异步函数 + 并发队列

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSLog(@"-------------begin");
        // 1, 获取并发队列 : DISPATCH_QUEUE_CONCURRENT
    //    dispatch_queue_t syncSerial = dispatch_queue_create("WilliamAlex大叔", DISPATCH_QUEUE_CONCURRENT);
    
        // 可以直接使用全局的并发队列(第一个0:表示系统默认即可. 第二个0:表示预留字段)
        dispatch_queue_t globalQuque = dispatch_get_global_queue(0, 0);
    
        // 将任务添加到队列中
    
        dispatch_async(globalQuque, ^{
    
            for (NSInteger i = 0; i < 10; i++) {
    
                NSLog(@"1WilliiamAlex大叔%@",[NSThread currentThread]);
            }
        });
    
        dispatch_async(globalQuque, ^{
    
            for (NSInteger i = 0; i < 10; i++) {
    
                NSLog(@"2WilliiamAlex大叔%@",[NSThread currentThread]);
            }
        });
    
        NSLog(@"-------------End");
    }
    

    打印结果(部分打印)

    2016-03-22 11:09:43.346 01 - 多线程[774:33864] -------------begin
    2016-03-22 11:09:43.347 01 - 多线程[774:33864] -------------End
    2016-03-22 11:09:43.347 01 - 多线程[774:33908] 2WilliiamAlex大叔<NSThread: 0x7fa45b43d030>{number = 2, name = (null)}
    2016-03-22 11:09:43.347 01 - 多线程[774:33907] 1WilliiamAlex大叔<NSThread: 0x7fa45b61fcf0>{number = 3, name = (null)}
    2016-03-22 11:09:43.348 01 - 多线程[774:33908] 2WilliiamAlex大叔<NSThread: 0x7fa45b43d030>{number = 2, name = (null)}
    2016-03-22 11:09:43.348 01 - 多线程[774:33907] 1WilliiamAlex大叔<NSThread: 0x7fa45b61fcf0>{number = 3, name = (null)}
    2016-03-22 11:09:43.348 01 - 多线程[774:33908] 2WilliiamAlex大叔<NSThread:
    
    • 注意 : 异步 + 并发:开启了子线程,执行顺序是乱序的,并且将任务添加到队列中后就立即返回了,不用等待任务的执行.
    • 补充
    • GCD中还提供了一个函数来执行任务,但是它有点特殊.
    void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
    void dispatch_barrier_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
    void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
    void dispatch_barrier_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
    
    
    • 该函数的使用场景 : 有时候我们需要让某个任务单独执行,也就是说在执行的时候,不允许其他的任务执行.这时候我们就可以使用dispatch_barrier.
    dispatch_barrier示例
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSLog(@"同步函数之前 %@",[NSThread currentThread]);
        /**
         * 参数 1: 队列的名称  参数 2: 队列的类型
         * DISPATCH_QUEUE_SERIAL NULL  // 表示串行队列
         * DISPATCH_QUEUE_CONCURRENT  // 表示并发队列
         */
        dispatch_queue_t queue = dispatch_queue_create("我的微博是@WilliamAlex大叔", DISPATCH_QUEUE_CONCURRENT);
    
            dispatch_sync(queue, ^{
    
             NSLog(@"同步函数中%@",[NSThread currentThread]);
            });
    
        dispatch_barrier_async(queue, ^{
    
            NSLog(@"猜猜我在哪里执行%@",[NSThread currentThread]);
        });
    
            NSLog(@"同步函数之后 %@",[NSThread currentThread]);
    
    }
    
    
    打印结果
    2016-03-21 23:44:55.517 01 - 多线程[1196:116016] 同步函数之前 <NSThread: 0x7ffea8c02a00>{number = 1, name = main}
    2016-03-21 23:44:55.517 01 - 多线程[1196:116016] 同步函数中<NSThread: 0x7ffea8c02a00>{number = 1, name = main}
    2016-03-21 23:44:55.517 01 - 多线程[1196:116016] 同步函数之后 <NSThread: 0x7ffea8c02a00>{number = 1, name = main}
    2016-03-21 23:44:55.518 01 - 多线程[1196:116084] 猜猜我在哪里执行<NSThread: 0x7ffea8e5ba80>{number = 2, name = (null)}
    
    
    • 从打印结果中获取到的信息 : 当我们使用dispatch_barrier将任务添加到队列中,队列中的任务会在前面所有的任务执行完毕后执行,当dispatch_barrier执行任务过程中,其它任务是不允许执行的,直到barrier任务执行完成
    • 还有一个有趣的信息,从打印出来的结果中可以看到dispatch_barrier上执行的任务都是在子线程上执行的.所以,dispatch_barrier是将任务添加到并发队列中的.
    知识拓展
    • dispatch_barrier最典型的使用场景是读写问题,NSMutableDictionary在多个线程中如果同时写入,或者一个线程写入一个线程读取,会发生无法预料的错误。但是他可以在多个线程中同时读取。如果多个线程同时使用同一个NSMutableDictionary。怎样才能保护NSMutableDictionary不发生意外呢?
    - (void)setObject:(id)anObject forKey:(id
    )aKey
    {
        dispatch_barrier_async(self.concurrentQueue, ^{
            [self.mutableDictionary setObject:anObject forKey:aKey];
        });
    }
    
    - (id)objectForKey:(id)aKey
    {
        __block id object = nil;
        dispatch_sync(self.concurrentQueue, ^{
            object = [self.mutableDictionary objectForKey:aKey];
        });    return  object;
    }
    
    • 当NSMutableDictionary写入的时候,我们使用dispatch_barrier_async,让其单独执行写入操作,不允许其他写入操作或者读取操作同时执行。当读取的时候,我们只需要直接使用dispatch_sync,让其正常读取即可。这样就可以保证写入时不被打扰,读取时可以多个线程同时进行

    • 总结 : 使用dispatch_barrier的前提是,队列必须是并发队列,但是这个queue(队列)不能是全局队列.

    • dispatch_barrier的最根本的原理 : 只有它前面所有的任务都执行完毕之后才会执行dispatch_barrier中队列的任务,并且在执行任务过程中,其它任务是不允许执行的,也就是说,当它在执行过程中,只有它在执行.

    创建队列

    • 创建队列 : 使用dispatch_queue_create函数创建的队列.
    /*
        参数 1: 队列的名称
        参数 2: 队列的类型
        * NULL                         // 表示串行队列
        * DISPATCH_QUEUE_SERIAL       // 表示串行队列
        * DISPATCH_QUEUE_CONCURRENT  // 表示并发队列
    */
        dispatch_queue_t queue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
    
    • 串行队列
    
    使用dispatch_queue_create函数创建串行队列
    // 创建串行队列(队列类型传递NULL或者DISPATCH_QUEUE_SERIAL)
    
    // 方式 1 : NULL
     dispatch_queue_t queue = dispatch_queue_create(@"com.William.Alex", NULL);
    
    // 方式 2 : DISPATCH_QUEUE_SERIAL
     dispatch_queue_t queue = dispatch_queue_create(@"com.William.Alex", DISPATCH_QUEUE_SERIAL);
    
    使用主队列(跟主线程相关联的队列)
    主队列是GCD自带的一种特殊的串行队列
    放在主队列中的任务,都会放到主线程中执行
    使用dispatch_get_main_queue()获得主队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    
    • 并发队列
     dispatch_queue_t queue = dispatch_queue_create(@"com.William.Alex", DISPATCH_QUEUE_CONCURRENT);
    
    • 全局并发队列
    GCD默认已经提供了全局的并发队列,供整个应用使用,可以无需手动创建
    使用dispatch_get_global_queue函数获得全局的并发队列
    dispatch_queue_t dispatch_get_global_queue(
    dispatch_queue_priority_t priority, // 队列的优先级
    unsigned long flags); // 用0即可
    
    获得全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    全局并发队列的优先级
    #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
    #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中)
    #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
    #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
    
    

    知识拓展

    • 全局并发队列函数中的两个参数:
    • identifier: 用以标识队列优先级,推荐用qos_class枚举作为参数,也可以使用dispatch_queue_priority_t
    • flags: 预留字段,传入任何非0的值都可能导致返回NULL.
    • 获取主队列
    // 直接获取主队列
    NSLog(@"主队列上的任务都是在主线程上执行的%@",dispatch_get_main_queue());
    
    主队列的使用场景
    • 主线程是我们最常用的线程,GCD提供了非常简单的获取主线程队列的方法.我们使用主队列或者是主线程主要是执行一些UI界面的操作(比如:UI界面的属性等事件),记住:只要是一些耗时的操作一般都是放在子线程上执行,不耗时的操作就放在主线程上执行.
    dispatch_async(dispatch_get_main_queue(), ^{
        // 刷新UI界面操作
    });
    
    • 执行加入到主线程队列的block,App会调用dispatch_main(), NSApplicationMain(),或者在主线程使用CFRunLoop。

    • 有的朋友习惯将参数都设置为0

    很多时候我们喜欢将0或者NULL传入作为参数
    
    dispatch_get_global_queue(NULL, NULL)
    
    由于NULL等于0,也就是DISPATCH_QUEUE_PRIORITY_DEFAULT,所以返回的是默认优先级
    

    网络延迟

    dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
    void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *context, dispatch_function_t work);
    
    • 几种延迟操作
      • 定时器(NSTimer)
      • GCD(dispatch_after)
      • 调用NSObject方法(performeSelector)
    • NSTimer定时器
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    
    • performeSelector
    // 直接调用NSObject方法,,performeSelector方法也是比较常见具体实现是.
    // nonnull : 表示参数不能设置为空
    // nullable : 表示参数可以设置为空
    [self performSelector:(nonnull SEL) withObject:(nullable id) afterDelay:(NSTimeInterval)]
    
    [self performSelector:(nonnull SEL) withObject:(nullable id) afterDelay:(NSTimeInterval) inModes:(nonnull NSArray<NSString *> *)]
    
    • GCD(dispatch_after)
    /*
    #define DISPATCH_TIME_NOW (0ull)
    #define DISPATCH_TIME_FOREVER (~0ull)
    注意 : 不能传DISPATCH_TIME_FOREVER,会一直阻塞线程
    */
    dispatch_after(dispatch_time_t when, dispatch_queue_t queue, ^(void)block)
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //  2.0
        });
    
    

    总结延迟操作

    • 函数说明: 2.0秒之后block中的任务会加到队列中,两个函数的第一个参数都是when,表示时间,当我们传入DISPATCH_TIME_NOW当前函数就相当于是一个dispatch_async(异步函数).值得注意的是,我们不能传入DISPATCH_TIME_FOREVER,因为这样会造成线程的阻塞.

    • 参数说明:dispatch_after接收block作为参数,系统持有block,block中self不需要weak。dispatch_after_f接收work函数作为参数,context作为work函数的第一个参数

    • 注意: 需要注意的是这里的延时并不精确的,因为加入队列不一定会立即执行。延时1s可能会1.5s甚至2s之后才会执行。

    dispatch_queue_set_specific 和 dispatch_queue_get_specific

    dispatch_queue_set_specific的使用场景

    • 当我们需要将某些东西关联在队列上,比如说我们想在队列上存储一些东西,又或者我们想区分两个队列。GCD提供了dispatch_queue_set_specific方法,通过key,将context关联到queue上
    void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor);
    

    参数说明

    • queue:需要关联的queue,不允许传入nil
    • key:队列的标识
    • context:要关联的内容,可以为nil
    • destructor:释放context的函数,当新的context被设置时,destructor会被调用

    dispatch_queue_get_specific的使用场景

    • 有存就有取,将context关联到queue上之后,可以通过dispatch_queue_get_specific或者dispatch_get_specific方法将值取出来。
    void *dispatch_queue_get_specific(dispatch_queue_t queue, const void *key);
    void *dispatch_get_specific(const void *key);
    

    dispatch_queue_get_specific的解释

    • 根据queue和key取出context,queue参数不能传入全局队列

    • dispatch_get_specific: 根据唯一的key取出当前queue的context。如果当前queue没有key对应的context,则去queue的target queue取,取不着返回nil.

    • 如果对全局队列取,也会返回nil
      iOS 6之后dispatch_get_current_queue()被废弃,如果我们需要区分不同的queue,可以使用set_specific方法。根据对应的key是否有值来区分

    一个人的力量是有限的,如果文章有有错误,希望大家指出来,相互帮助,相互进步.感谢@小笨狼Lc提供资料,想要了解更多知识可以点进连接,关注CocoaChina的微信公众号.http://www.cocoachina.com/ios/20160225/15422.html
    后续会持续更新.....加油!!!!!

    相关文章

      网友评论

        本文标题:多线程第一弹 - GCD

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