iOS底层探究-----GCD基础&源码分析 中

260 阅读5分钟

前言

上篇文章,对多线程的几种形式做了探索,也提到了GCD的使用,那么这篇文章将对GCD进行重点探索。

资源准备

GCD的简介

  • GCD --- 全称是Grand Central Dispatch

  • C语言,提供例如非常强大的函数;

  • GCD作用:将任务添加到队列,并指定任务执行的函数。

GCD的优势

  • GCD是苹果公司为多核的并行运算提出的解决方案;

  • GCD会自动利用更多的CPU内核(比如双核、四核);

  • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程);

  • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。

GCD的基础

其基本用法,代码如下:

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

还原最基础的写法,可拆分成任务、队列和函数:

  • 使用dispatch_block_t创建任务:

    • 任务使用block封装;

    • 任务的block没有参数,也没有返回值;

  • 使用dispatch_queue_t创建队列;

  • 将任务添加到队列,并指定执行任务的函数dispatch_async

下面用代码演示下:

- (void)syncTest{ 
    // 任务 
    dispatch_block_t block = ^{ 
        NSLog(@"hello GCD"); 
    }; 
    
    // 串行队列 
    dispatch_queue_t queue = dispatch_queue_create("com.lg.cn", NULL); 
    
    // 函数 
    dispatch_async(queue, block); 
}

函数

GCD中执行任务的方式有两种,同步执行和异步执行:

  • dispatch_sync:同步函数

  • dispatch_async:异步函数

dispatch_sync

同步函数dispatch_sync的特性:

  • 必须等待当前语句执行完毕,才会执行下一条语句;

  • 不会开启线程,即不具备开启新线程的能力;

  • 在当前线程中执行 block 任务;

dispatch_async

异步函数dispatch_async的特性:

  • 不用等待当前语句执行完毕,就可以执行下一条语句;

  • 会开启线程执行block任务,即具备开启新线程的能力(但并不一定开启新线程,这个与任务所指定的队列类型有关);

  • 异步是多线程的代名词;

二者的区别

  • 是否等待队列的任务执行完毕;

  • 是否具备开启新线程的能力。

队列

367a8282f36d4739a1f87fa13bfd80fb~tplv-k3u1fbpfcp-watermark.image.png

队列分为串行队列并发队列,用来存放任务。队列是一种数据结构,属于特殊的线性表,遵循先进先出(FIFC)原则。新任务被插入到队尾,而任务的读取从队首开始。每读取一个任务,则队列中释放一个任务;

GCD中,还提供了两个特殊的队列,分别是主队列和全局并发队列。主队列属于串行队列,而全局并发队列属于并发队列;

队列和线程并没有关系,队列负责任务的调度,任务的执行依赖于线程,优先调度的任务不一定优先执行;

串行队列

image.png

串行队列:Serial Dispatch Queue

  • 每次只有一个任务被执行,等待上一个任务执行完毕再执行下一个;

  • 只开启一个线程,同一时刻只调度一个任务执行;

  • 使用DISPATCH_QUEUE_SERIAL创建串行队列;

  • DISPATCH_QUEUE_SERIAL也可传入NULL,默认创建为串行队列。

dispatch_queue_t serialQueue1 = dispatch_queue_create("com.lg.serial", NULL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("com.lg.serial", DISPATCH_QUEUE_SERIAL);

并发队列

image.png

并发队列:Concurrent Dispatch Queue

  • 一次可以并发执行多个任务;

  • 开启多个线程,同一时刻可以调度多个任务执行;

  • 使用DISPATCH_QUEUE_CONCURRENT创建并发队列;

  • 并发队列的并发功能只有在异步函数下才有效。

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.lg.concurrent", DISPATCH_QUEUE_CONCURRENT);

主队列

主队列:Main Dispatch Queue

  • 主队列:GCD提供的特殊的串行队列;

  • 专门用来在主线程上调度任务的串行队列,依赖于主线程、主Runloop,在main函数调用之前自动创建;

  • 不会开启线程;

  • 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度;

  • 使用dispatch_get_main_queue获得主队列;

  • 通常在返回主线程更新UI时使用。

dispatch_queue_t mainQueue = dispatch_get_main_queue();

全局并发队列

全局并发队列:Global Dispatch Queue

dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
  • GCD提供的默认的并发队列;

  • 为了方便开发者使用,苹果提供了全局队列;

  • 在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列;

  • 使用dispatch_get_global_queue获取全局并发队列,最简单的是dispatch_get_global_queue(0, 0)

    • 参数1表示队列优先级,默认优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT = 0,被服务质量quality of service取代;

    • 参数2为标记,是为了未来使用保留的。所以这个参数应该永远指定为0

优先级从高到低,对应服务质量:

  • DISPATCH_QUEUE_PRIORITY_HIGHQOS_CLASS_USER_INITIATED

  • DISPATCH_QUEUE_PRIORITY_DEFAULTQOS_CLASS_DEFAULT

  • DISPATCH_QUEUE_PRIORITY_LOWQOS_CLASS_UTILITY

  • DISPATCH_QUEUE_PRIORITY_BACKGROUNDQOS_CLASS_BACKGROUND

日常开发中,主队列 + 全局并发队列的使用:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    //执行耗时操作 
    dispatch_async(dispatch_get_main_queue(), ^{
        //回到主线程进行UI操作 
    }); 
});

队列与函数的搭配使用

8CDC29A1-8758-4D0F-93F2-01F1D04F35F5.png

串行队列和函数的搭配

同步函数

  • 不会开启线程,在当前线程执行;
  • 任务串行执行,一个接一个。 代码验证:
/**
 串行同步队列 : FIFO: 先进先出
 */
- (void)serialSyncTest{
    //1:创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i<5; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

打印结果:

3A084A8F-AD52-4CD5-9DD8-E3AE7AC9D752.png

异步函数

  • 开启新线程;
  • 任务串行执行,一个接一个; 代码验证:
/**
 串行异步队列
 */
- (void)serialAsyncTest{

    //1:创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i < 5; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

打印结果:

DE54E31F-FEF9-46E4-97A2-7845059D3E7C.png

并发队列和函数的搭配

同步函数

  • 不会开启线程,在当前线程执行;
  • 任务串行执行,一个接一个。 代码验证:
/**
 同步并发 : 堵塞 同步锁  队列 : resume supend   线程 操作, 队列挂起 任务能否执行
 */
- (void)concurrentSyncTest{

    //1:创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 5; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

打印结果:

A504EBB1-E9C6-4AC8-833C-9EEA9890F7E6.png

异步函数

  • 开启新线程;
  • 任务异步执行,没有顺序,和CPU调度有关。 代码验证:
/**
 异步并发: 有了异步函数不一定开辟线程
 */
- (void)concurrentAsyncTest{

    //1:创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<100; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

打印结果:

A7C19E4F-3992-4FDE-A14A-448DC45248EB.png

主队列和函数的搭配

同步函数

  • 主队列的作用,专门用来在主线程上调度任务的串行队列;

  • 主队列中增加同步函数,导致主线程需要等待同步函数完成后再执行;

  • 由于主队列是特殊的串行队列,同步函数需要等待主线程完成后再执行;

  • 所以,两个任务相互等待,产生死锁。 代码验证: 会直接崩溃:

EE4DDC29-7793-4CC9-8CBB-9EA68320DE64.png

异步函数

  • 不会开启线程,在当前线程执行;
  • 任务串行执行,一个接一个。 代码验证:
/**
 主队列异步
 不会开线程 顺序
 */
- (void)mainAsyncTest{

    dispatch_queue_t queue = dispatch_get_main_queue();
    for (int i = 0; i<5; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d:%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

打印结果:

15466AC2-67D5-4196-8F2B-B522770C7690.png

全局队列和函数的搭配

同步函数

  • 不会开启线程,在当前线程执行;
  • 任务串行执行,一个接一个。 代码验证:
/**
 全局同步
 全局队列:一个并发队列
 */

- (void)globalSyncTest{
    for (int i = 0; i<5; i++) {
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

打印结果:

7793D89B-673A-43CA-B78B-4C77E92A90F2.png

异步函数

  • 开启新线程;
  • 任务异步执行,没有顺序,和CPU调度有关。 代码验证:
/**
 全局异步
 全局队列:一个并发队列
 */
- (void)globalAsyncTest{

    for (int i = 0; i<20; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

打印结果:

78A88D86-29A7-4B21-8EB0-86664B0E410D.png

线程死锁

所谓线程死锁,是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行

6C95ABDA-5C90-4C9C-AFDB-8208C6E2D9D7.png

死锁会导致奔溃:

D657F710-CF51-45B3-92E1-A680317A721A.png

再查看堆栈信息,里面就会有 _dispatch_sync_f_slow 的调用,即为死锁的异常

面试题解析

面试题 1

- (void)wbinterDemo {
    dispatch_queue_t queue = dispatch_queue_create("com.lg.cn", DISPATCH_QUEUE_CONCURRENT);

    NSLog(@"顺序1:%@",[NSThread currentThread]);

    dispatch_async(queue, ^{
        NSLog(@"顺序2:%@",[NSThread currentThread]);
        dispatch_async(queue, ^{
            NSLog(@"顺序3:%@",[NSThread currentThread]);
        });
        NSLog(@"顺序4:%@",[NSThread currentThread]);
    });
    NSLog(@"顺序5:%@",[NSThread currentThread]);
}

分析:首先知道是并发队列 DISPATCH_QUEUE_CONCURRENT,最先执行顺序1的打印,接着往下走,异步线程不会阻塞线程,执行到异步线程里面,但是异步线程返回的信息不确定,也不用等待,所以直接往下执行,打印顺序5,剩下的就是异步线程里面的内容了,在异步线程里面没有排序操作,所以就按顺序直接打印顺序2顺序4,里面还有个异步线程,所以最后打印顺序3

查看打印结果:

8A13C3BB-9447-4B1B-BF0F-E81578FAEB31.png

面试题2

- (void)wbinterDemo1{
    dispatch_queue_t queue = dispatch_queue_create("com.lg.cn", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"顺序1:%@",[NSThread currentThread]);
    
    dispatch_async(queue, ^{
    
        NSLog(@"顺序2:%@",[NSThread currentThread]);
        dispatch_sync(queue, ^{
        
            NSLog(@"顺序3:%@",[NSThread currentThread]);
        });
        
        NSLog(@"顺序4:%@",[NSThread currentThread]);
    });   
    
    NSLog(@"顺序5:%@",[NSThread currentThread]);
}

分析:首先知道是并发队列 DISPATCH_QUEUE_CONCURRENT,最先执行顺序1的打印,接着往下走,异步线程不会阻塞线程,执行到异步线程里面,但是异步线程返回的信息不确定,也不用等待,所以直接往下执行,打印顺序5,剩下的就是异步线程里面的内容了,在异步线程里面没有排序操作,但是里面还有个同步线程,串行执行,所以就按顺序直接打印顺序2顺序3顺序4

打印结果:

F50E36AA-A9D5-4690-8A8B-C37668BCB1FB.png

面试题3

- (void)wbinterDemo4 {
    dispatch_queue_t queue = dispatch_queue_create("com.lg.cn", NULL);

    NSLog(@"顺序1:%@",[NSThread currentThread]);

    dispatch_async(queue, ^{

        NSLog(@"顺序2:%@",[NSThread currentThread]);

        dispatch_sync(queue, ^{

            NSLog(@"顺序3:%@",[NSThread currentThread]);
        });

        NSLog(@"顺序4:%@",[NSThread currentThread]);
    });
    
    NSLog(@"顺序5:%@",[NSThread currentThread]);
}

分析:首先确定是串行队列,顺序1 顺序5在主队列,不受影响,异步函数先执行顺序2,但是顺序4要等待同步函数优先执行;由于使用串行队列,同步函数又需要异步函数执行完成后才能执行,所以,两个函数相互等待,产生死锁。

打印结果:

6A5E31EF-0C5C-48E1-8748-9C026A1B6735.png

面试题4

- (void)wbinterDemo{

    dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{
        NSLog(@"顺序1");
    });

    dispatch_async(queue, ^{
        NSLog(@"顺序2");
    });

    dispatch_sync(queue, ^{ NSLog(@"顺序3"); });

    NSLog(@"顺序0");

    dispatch_async(queue, ^{
        NSLog(@"顺序7");
    });

    dispatch_async(queue, ^{
        NSLog(@"顺序8");
    });
    
    dispatch_async(queue, ^{
        NSLog(@"顺序9");
    });
}

有4个选项:

  • A: 1230789
  • B: 1237890
  • C: 3120798
  • D: 2137890 正确答案:A、C

首先确定是串行队列,执行顺序1顺序2在异步函数中,所以顺序不确定;顺序3在同步函数中,顺序0在主线程,所以顺序3一定在顺序0的前面; 执行的顺序7顺序8顺序9一定会在顺序3顺序0之后,但它们在异步函数中,所以顺序也是不确定的。

题目5

队列的类型有几种?

正确答案:两种

  • 串行队列:serialQueuemainQueue

  • 并发队列:concurrentQueueglobalQueue

//串行队列 - Serial Dispatch Queue 
dispatch_queue_t serialQueue = dispatch_queue_create("com.lg.cn", NULL); 

//并发队列 - Concurrent Dispatch Queue 
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.lg.cn", DISPATCH_QUEUE_CONCURRENT); 

//主队列 - Main Dispatch Queue 
dispatch_queue_t mainQueue = dispatch_get_main_queue(); 

//全局并发队列 - Global Dispatch Queue 
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

题目 6

@property (atomic, assign) int num; 

- (void)MTDemo {
    while (self.num < 5) { 
        dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
            self.num++;
        }); 
    } 
    NSLog(@"end : %d",self.num); 
}
  • 正确答案:>= 5
  • 首先一点num的值不能小于5,因为小于5的时候,是出不来while循环的。
  • 上面添加的都是异步函数,这就导致num值为5的时候,可能还有任务在执行self.num++操作,所以num的值有可能大于5

面试题 7

for (int i= 0; i<10000; i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.num++;
        });
    }

    NSLog(@"end : %d",self.num);
}
  • 这个面试题和上面的有所不同,这里的条件判断和自增不是用的同一个变量,代码会循环10000次不停的添加self.num++任务,这就导致我们执行++操作时,可能num的值已经变大了,但是我们还是加的之前的小值,所以最终会<= 10000