美文网首页
第二十九节—GCD(一)

第二十九节—GCD(一)

作者: L_Ares | 来源:发表于2020-11-22 03:17 被阅读0次

    本文为L_Ares个人写作,以任何形式转载请表明原文出处。

    这里来说GCD是因为GCD和线程,内存,RunLoop都挂钩,本来是想先说的,但是因为前面几个内容的因素,所以先简单的介绍了前面的几个内容,然后再开始GCD。前面几个内容都是大概的介绍,并没有完全的更新完成,后续还会有他们的章节。

    那么本节探索GCD其实是要从libdispatch源码入手的。在dyld的章节提到过libdispatch,应该知道它是libSystem动态库初始化之后的第二个进行初始化的动态库,经过libdispatch的初始化,才会找到libobjc_objc_init初始化。

    先不说那么多,先介绍GCD的基本概念。

    一、GCD概念

    1. 基本概念

    • 英文全称 : Grand Central Dispatch

    • 中文全称 : 多线程优化技术(实际上直接翻译就是中央调度,更直接的说明了GCD的性质)

    • 语言 : 纯C语言,提供了非常多很强大的函数。

    • 使用方式 : 将任务添加到队列,并且指定执行任务的函数。

    • 优势 :

    1. GCD是苹果公司为多核的并行运算提出的解决方案。
    2. GCD会自动利用更多的CPU内核,比如用双核、四核。
    3. GCD会自动管理线程的生命周期(创建线程、调度线程、销毁线程)。
    4. 只需要告诉GCD想要执行什么任务,而不需要编写任何的线程管理的代码。

    一句话说GCD : GCD = 任务 + 队列 + 函数

    2. GCD常用的概念

    2.1 任务
        dispatch_block_t mission_block = ^{
            NSLog(@"这是一个GCD的任务");
        };
    

    任务就是你要执行的事件。

    2.2 队列
    dispatch_queue_t jd_queue = dispatch_queue_create("jd_gcd_queue", NULL);
    
    1. 队列本身是一种特殊的线性表。
    2. 遵循FIFO(first in first out)先进先出原则。
    3. 先进先出的意思就是新添加的任务总是会被插入到队列的末尾,而读取任务则是从队列的头部开始。每取一个任务,队列就释放一个任务。
    4. GCD中可以理解为装载任务的数据结构。

    在GCD中队列分为以下两种 :

    • 串行队列(Serial Dispatch Queue) : 每次只有一个任务被执行,按照任务在队列中的顺序,一个一个的按照顺序执行。串行队列只开启一条线程,一个任务执行完毕之后,再执行下一个任务。
    • 并发队列(Concurrent Dispatch Queue) : 可以让多个任务并发(同时)执行。可以开启多个线程,并且同时执行任务。并发队列的同时执行任务只有在dispatch_async函数下才有效。
    图1.2.0.png
    2.3 函数
    异步函数,开启多线程
    dispatch_async(jd_queue, mission_block);
    同步函数,不开启多线程
    dispatch_sync(jd_queue, mission_block);
    
    1. 同步函数(sync) :
    • 同步添加任务到指定的队列当中,在添加的任务执行完成之前,其他任务会一直处于等待状态,直到同步函数添加的任务执行完成之后,才会执行其他的任务。
    • 同步函数,只在当前线程中执行任务,不具备开启新线程的能力。
    1. 异步函数(async) :
    • 异步添加任务到指定队列中,异步函数添加任务到指定队列后,不等待任务的执行,直接走掉,不影响当前线程中其他任务的执行。
    • 异步函数,具备开启新线程的能力,可以在新线程中执行任务。(但我没说一定就会开启线程,只是具备这个能力,什么时候开启新线程则跟队列有关)。
    图1.2.1.png

    二、 GCD的简单使用方法

    上面的基本概念中介绍了CGD = 任务 + 队列 + 函数。那么GCD的使用方法就很简单了,按照内容的多少进行的排序介绍 :

    1. 队列的创建

    1.1 自己创建的队列
    dispatch_queue_t dispatch_queue_create(const char *_Nullable label,
                                           dispatch_queue_attr_t _Nullable attr);
    

    利用GCD给我们的函数,它将会返回一个dispatch_queue_t类型的变量,这就是队列。

    参数 :

    • const char *_Nullable label : 队列的名称,是队列的唯一标识符。

    • dispatch_queue_attr_t _Nullable attr : 队列的类型。

      • DISPATCH_QUEUE_SERIAL : 串行队列,是一个宏定义的NULL,所以串行队列在创建的时候可以将参数设置成NULL

      • DISPATCH_QUEUE_CONCURRENT : 并发队列,也是宏定义的DISPATCH_GLOBAL_OBJECT

    举例 :

    串行队列
    dispatch_queue_t serial_queue = dispatch_queue_create("jd_gcd_serial_queue", DISPATCH_QUEUE_SERIAL);
    并发队列
    dispatch_queue_t concurrent_queue = dispatch_queue_create("jd_gcd_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
    
    1.2 GCD提供的队列

    除了自己创建的队列,队列中存在两个GCD主动提供的,常见的队列 :

    1. 对于串行队列 : GCD默认提供了主队列(Main Dispatch Queue)
      主队列的特点 :
      • 主队列直接通过GCD的api : dispatch_get_main_queue()获取。
      • 所有加入到主队列中的任务都会在主线程中被执行。
    2. 对于并发队列 : GCD默认提供了全局并发队列(Global Dispatch Queue)
      全局并发队列的特点 :
      • 全局并发队列通过GCD的api : dispatch_get_global_queue(intptr_t identifier, uintptr_t flags);获取。

    对于全局并发队列的两个参数 :

    • intptr_t identifier : 队列的优先级。有以下参数可选 :
      • 高优先级 : DISPATCH_QUEUE_PRIORITY_HIGH
      • 默认优先级 : DISPATCH_QUEUE_PRIORITY_DEFAULT
      • 低优先级 : DISPATCH_QUEUE_PRIORITY_LOW
      • 后台运行队列 : DISPATCH_QUEUE_PRIORITY_BACKGROUND
    • uintptr_t flags : 苹果官方说留作以后使用,传0以外的值有可能会返回NULL

    对于这两个GCD提供的队列举例 :

    获取主队列
    dispatch_queue_t main_queue = dispatch_get_main_queue();
    获取全局并发队列,设置默认优先级
    dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    

    2. 任务和函数的创建

    这里可以放到一起说,因为存在任务可以直接写在函数自带的block里面的情况。任务的创建在上面已经说过了,就是一个dispatch_block_t,给一个block即可,所以你也可以理解为任务就是一个dispatch_block_t :

        dispatch_block_t mission_block = ^{
            NSLog(@"这是一个GCD的任务");
        };
    

    说说这两个函数,同步函数(dispatch_sync)异步函数(dispatch_async)的创建方法 :

        dispatch_sync(queue, ^{
            NSLog(@"直接使用同步函数的block存放任务 --- 当前线程 : %@",[NSThread currentThread]);
        });
        dispatch_async(queue, ^{
            NSLog(@"直接使用异步函数的block存放任务 --- 当前线程 : %@",[NSThread currentThread]);
        });
    

    三、队列和函数组合

    1. 无嵌套,GCD队列和函数的组合

    这里一定要明确上面的队列和函数的概念。

    根据上面队列和函数的描述,再加上代码默认都是在主线程的主队列中执行,我们可以组合出6种函数+任务的方式 :

    区别 串行队列 并发队列 主队列
    同步函数 不开启新线程,
    按顺序执行队列中的任务
    不开启新线程,
    按顺序执行队列中的任务
    主线程中执行会造成死锁,不执行任务。
    其他线程执行则不会出错。
    异步函数 开启1条新线程,
    按顺序执行队列中的任务
    按情况开启新线程,
    并发执行队列中的任务
    不开启新线程,
    按顺序执行任务。

    主线程 + 主队列 + 同步函数死锁的原因 :

    1. 主队列是串行队列,队列中的任务要按序执行,任务都会被放在主线程中进行。
    2. 在主队列中加入了一个同步函数,完成一个向主队列中新增的任务。
    3. 同步函数必须执行完成新增的任务。
    4. 新增的任务在主队列中被放在了同步函数后面。
    5. 同步函数一定要执行任务,也就是要把排在它后面的新增任务完成,但是因为是主队列是串行队列,所以不允许后面的新增任务在同步函数没执行完成之前就被执行,这就造成了互相等待。
    图3.1.0.png

    如图1.4.0所示,同步函数和主队列中的任务互相等待,造成了主线程的死锁。

    对应的错误代码 :

        这是错误代码,千万别在主线程里面这么用
        dispatch_queue_t main_queue = dispatch_get_main_queue();
        dispatch_sync(main_queue, ^{
            NSLog(@"1 --- %@",[NSThread currentThread]);
        });
    

    解决办法 :

    1. 自定义一个串行队列是可以的。
    2. 在其他的线程中执行。

    2. 有嵌套,GCD队列和函数的组合

    区别 异步函数 + 并发队列
    嵌套同一个并发队列
    同步函数 + 并发队列
    嵌套同一个并发队列
    异步函数 + 串行队列
    嵌套同一个串行队列
    同步函数 + 串行队列
    嵌套同一个串行队列
    同步 异步可能开启一个新线程,
    嵌套的同步不会开启新线程,在异步开启的新线程中
    串行执行任务
    不开启新线程,
    串行执行任务
    造成死锁 造成死锁
    异步 开启新线程,
    嵌套的异步函数也会开启新线程,
    并发执行任务
    同步函数不会开启新线程,
    异步函数会开启新线程,
    并发执行任务
    异步函数会开启新线程,
    串行执行任务
    异步函数会开启新线程,
    串行执行任务

    这里造成死锁的原因和主线程 + 主队列 + 同步函数是一样的道理。所以可以总结为 :

    • 同步函数 : 添加任务,并且必须等着任务执行完成,才算同步函数这个任务结束。
    • 异步函数 : 只管把任务添加到队列中,添加完成了不需要管这个任务是否执行完毕,添加就是异步函数的任务。任务的执行不会影响异步函数完成任务。

    四、GCD的简单使用

    1. 同步函数 + 串行队列

    - (void)jd_gcd_sync_serial
    {
        NSLog(@"\n同步函数 + 串行队列 开始:\n %@",[NSThread currentThread]);
        
        //创建一个串行队列
        dispatch_queue_t serial_queue = dispatch_queue_create("jd_gcd_sync_serial", DISPATCH_QUEUE_SERIAL);
        
        //同步函数向串行队列添加任务1
        dispatch_sync(serial_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n1 --- %@",[NSThread currentThread]);
        });
        
        //同步函数向串行队列添加任务2
        dispatch_sync(serial_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n2 --- %@",[NSThread currentThread]);
        });
        
        //同步函数向串行队列添加任务3
        dispatch_sync(serial_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n3 --- %@",[NSThread currentThread]);
        });
        
        NSLog(@"\n同步函数 + 串行队列 结束 :\n %@",[NSThread currentThread]);
        
    }
    

    执行结果 :

    图4.1.0.png

    结论 :

    同步函数 + 串行队列 :

    • 不会开启新的线程,就在当前线程执行队列中的任务。
    • 因为是串行队列,所以任务的执行都是有顺序的,遵守FIFO原则,先进先出。
    • 并且,同步函数会让自己添加的任务执行完毕,才允许当前线程执行下一个任务。

    2. 同步函数 + 并发队列

    - (void)jd_gcd_sync_concurrent
    {
        NSLog(@"\n同步函数 + 并发队列 开始:\n %@",[NSThread currentThread]);
        
        //创建一个并发对垒
        dispatch_queue_t concurrent_queue = dispatch_queue_create("jd_gcd_sync_concurrent", DISPATCH_QUEUE_CONCURRENT);
        
        //同步函数向并发队列添加任务1
        dispatch_sync(concurrent_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n1 --- %@",[NSThread currentThread]);
        });
        
        //同步函数向并发队列中添加任务2
        dispatch_sync(concurrent_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n2 --- %@",[NSThread currentThread]);
        });
        
        //同步函数向并发队列中添加任务3
        dispatch_sync(concurrent_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n3 --- %@",[NSThread currentThread]);
        });
        
        NSLog(@"\n同步函数 + 并发队列 结束:\n %@",[NSThread currentThread]);
        
    }
    
    

    执行结果 :

    图4.2.0.png

    结论 :

    同步函数 + 并发队列 :

    • 不会开启新的线程,就在当前线程执行队列中的任务。
    • 虽然是并发队列,任务允许同时执行,但是因为是同步函数,没有开启线程的能力,所以没有新的线程执行允许并发的任务。
    • 同步函数限制了并发队列的并发性,并且将自己的限制性发挥,只有执行完成同步函数后,线程才可以执行下一个任务。

    3. 异步函数 + 串行队列

    - (void)jd_gcd_async_serial
    {
        NSLog(@"\n异步函数 + 串行队列 开始:\n %@",[NSThread currentThread]);
        
        //创建一个串行队列
        dispatch_queue_t serial_queue = dispatch_queue_create("jd_gcd_async_serial", DISPATCH_QUEUE_SERIAL);
        
        //异步函数向串行队列添加任务1
        dispatch_async(serial_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n1 --- %@",[NSThread currentThread]);
        });
        
        //异步函数向串行队列添加任务2
        dispatch_async(serial_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n2 --- %@",[NSThread currentThread]);
        });
        
        //异步函数向串行队列添加任务3
        dispatch_async(serial_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n3 --- %@",[NSThread currentThread]);
        });
        
        NSLog(@"\n异步函数 + 串行队列 结束:\n %@",[NSThread currentThread]);
        
    }
    

    执行结果 :

    图4.3.0.png

    结论 :

    • 因为是多任务的异步,所以会开启线程,因为是串行队列,队列中的任务要按序执行,所以只需开启一条新线程。
    • 因为是异步添加任务,所以主线程不会做任何的等待,直接执行下一个任务,所以先打印开始和结束。
    • 因为是串行队列,所以添加的任务要按照添加的顺序被新开启的那一条线程执行。

    4. 异步函数 + 并发执行

    - (void)jd_gcd_async_concurrent
    {
        NSLog(@"\n异步函数 + 并发队列 开始:\n %@",[NSThread currentThread]);
        
        //创建一个并发队列
        dispatch_queue_t concurrent_queue = dispatch_queue_create("jd_gcd_async_concurrent", DISPATCH_QUEUE_CONCURRENT);
        
        //异步函数向并发队列添加任务1
        dispatch_async(concurrent_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n1 --- %@",[NSThread currentThread]);
        });
        
        //异步函数向并发队列添加任务2
        dispatch_async(concurrent_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n2 --- %@",[NSThread currentThread]);
        });
        
        //异步函数向并发队列添加任务3
        dispatch_async(concurrent_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n3 --- %@",[NSThread currentThread]);
        });
        
        NSLog(@"\n异步函数 + 并发队列 结束:\n %@",[NSThread currentThread]);
    }
    

    执行结果 :

    图4.4.0.png

    结论 :

    • 因为是异步,所以会开启线程,因为是并发队列,所以要根据队列中的任务情况开启多条线程。
    • 因为是异步,所以主线程中不会等待异步函数的执行,直接执行下一个任务,所以先执行了开始和结束。
    • 因为是并发队列,又是异步函数,所以任务可能被不同的线程同时执行。

    5. 同步函数 + 主队列

    上面有说过了,同步函数 + 主队列 + 主线程是会造成主线程死锁的。但是,如果在其他的线程执行同步函数 + 主队列,则不会造成主线程的死锁。

    死锁,主线程的情况 :

    - (void)jd_gcd_sync_main_queue
    {
        NSLog(@"\n同步函数 + 主队列 开始:\n %@",[NSThread currentThread]);
        
        //获取主队列
        dispatch_queue_t main_queue = dispatch_get_main_queue();
        
        //同步函数向主队列中添加任务1
        dispatch_sync(main_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n1 --- %@",[NSThread currentThread]);
        });
        
        //同步函数向主队列添加任务2
        dispatch_sync(main_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n2 --- %@",[NSThread currentThread]);
        });
        
        //同步函数向主队列添加任务3
        dispatch_sync(main_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n3 --- %@",[NSThread currentThread]);
        });
        
        NSLog(@"\n同步函数 + 主队列 结束:\n %@",[NSThread currentThread]);
    }
    

    执行结果 :

    图4.5.0.png

    不死锁,其他线程的情况 :

    - (void)jd_gcd_sync_other_thread_main_queue
    {
        [NSThread detachNewThreadSelector:@selector(jd_gcd_sync_main_queue) toTarget:self withObject:nil];
    }
    

    执行结果 :

    图4.5.1.png

    结论 :

    • 主队列+主线程+同步函数会导致主线程的死锁,因为同步函数和同步函数向主队列添加的任务会互相等待。
    • 主队列 + 其他线程 + 同步函数不会导致死锁,因为同步函数的执行不在主线程,而他其他线程,添加的任务则是在主线程中执行,两者不被同一个线程执行,也就不会造成互相等待。
    • 虽然其他线程中执行主队列+同步函数不会造成死锁,但是因为主队列是串行队列,添加的任务要依次执行,又因为是同步函数,所以和同步函数一条线程的后面的任务要等待同步函数执行完成,才可以执行。

    6. 异步函数 + 主线程

    - (void)jd_gcd_async_main_queue
    {
        NSLog(@"\n异步函数 + 主队列 开始:\n %@",[NSThread currentThread]);
        
        //获取主队列
        dispatch_queue_t main_queue = dispatch_get_main_queue();
        
        //异步函数向主队列中添加任务1
        dispatch_async(main_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n1 --- %@",[NSThread currentThread]);
        });
        
        //异步函数向主队列添加任务2
        dispatch_async(main_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n2 --- %@",[NSThread currentThread]);
        });
        
        //异步函数向主队列添加任务3
        dispatch_async(main_queue, ^{
            //模拟耗时操作
            [NSThread sleepForTimeInterval:2.f];
            //打印当前线程
            NSLog(@"\n3 --- %@",[NSThread currentThread]);
        });
        
        NSLog(@"\n异步函数 + 主队列 结束:\n %@",[NSThread currentThread]);
    }
    

    执行结果 :

    图4.6.0.png

    结论 :

    • 异步函数+主队列+主线程不会造成主线程的死锁。因为异步函数不等待添加的任务执行完毕,添加完成,即是异步函数执行完成。
    • 因为是主队列,所以添加的任务会在开始和结束任务的后面被加入,也就只能在其后面按序执行。

    五、总结

    本节主要就是介绍一下GCD的基本知识和基本使用,这些都是最基本需要掌握的,尤其是对队列和函数的概念一定要分的非常清楚 :

    函数 :

    • 同步函数 : 之所以叫同步,就是到同步函数这个任务的时候,它的完成就是保证它向队列中添加的任务要执行完成,同步函数不执行完成,则他所在的线程是不可以执行后面的任务的。
    • 异步函数 : 之所以是异步,就是因为它只管向队列中添加任务,并且分配线程去执行任务,但是它不会保证任务在什么时候执行完毕,也就是不会影响当前线程执行下面的任务。

    队列 :
    队列是一种特殊的链表结构,是数据结构!它的主要职责就是分辨任务的执行是否是有序的。

    • 串行队列 : 任务的执行必须严格遵守FIFO原则,先加入队列的任务先执行,后加入的任务后执行。
    • 并发队列 : 任务的执行是并发的,可以一起被执行,没有顺序的规定。

    相关文章

      网友评论

          本文标题:第二十九节—GCD(一)

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