iOS-底层原理24:GCD 之 函数与队列

在上一篇文章多线程中讲了些多线程基础知识,这篇文章以GCD为例进行深入分析。

1 GCD简介

GCD,全称Grand Central Dispatch(中央调度中心),纯C语言开发,提供了很多强大的函数

1.1 GCD的优势

  • GCD是苹果公司为多核的并行运算提出的解决方案
  • GCD会自动利用更多的CPU内核(比如双核、四核)
  • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。

程序员只需要告诉GCD想要执行的任务不需要编写任何线程管理相关代码(调度、销毁都不用管)

【重点】: 将任务添加到队列,并指定执行任务的函数

1.2 GCD核心

这里用一个简单的实例,来帮助更好的理解任务+队列+执行任务的函数

// 任务(block)
dispatch_block_t block = ^{
    NSLog(@"hello GCD");
};

// 队列(此处串行队列)
dispatch_queue_t queue = dispatch_queue_create("com.lbh.queue", DISPATCH_QUEUE_SERIAL);

// 执行任务的函数(此处异步函数)
dispatch_async(queue, block);
  • 使用dispatch_block_t创建任务
  • 使用dispatch_queue_t创建队列
  • 将任务添加到队列,并指定执行任务的函数dispatch_async

任务是使用block封装函数,没有参数返回值。任务创建好后,等待执行任务的函数将其放入队列

拓展:执行block,需要调用block(),这步调用,是执行任务的函数内部自动管理。
后面解析dispatch源码时,可以清楚知道调用时机

2 函数与队列

2.1 函数

在GCD中执行任务的方式有两种,同步执行异步执行,分别对应 同步函数dispatch_sync异步函数dispatch_async

2.1.1 dispatch_sync 同步函数

这个函数会把一个block加入到指定的队列中,而且会一直等到执行完blcok,这个函数才返回。因此在block执行完之前,调用dispatch_sync方法的线程是阻塞的

  • 必须等待当前语句执行完毕,才会执行下一条语句
  • 不会开启线程,即不具备开启新线程的能力
2.1.2 dispatch_async 异步函数

这个函数也会把一个block加入到指定的队列中,但是和同步执行不同的是,这个函数把block加入队列后不等block的执行就立刻返回了。

  • 不用等待当前语句执行完毕,就可以执行下一条语句
  • 开启线程执行block任务,即具备开启新线程的能力(但并不一定开启新线程,这个与任务所指定的队列类型有关)

2.2 队列

多线程中所说的队列(Dispatch Queue)是指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,遵循先进先出(FIFO)原则,即新任务总是被插入到队尾,而任务的读取从队首开始读取。

GCD的队列包含串行队列并行队列两种

2.2.1 串行队列

每次只有一个任务被执行,等待上一个任务执行完毕再执行下一个,即只开启一个线程(通俗理解:同一时刻只调度一个任务执行,类似单车道,汽车只能一辆辆排队通过)

使用dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);创建串行队列,其中的DISPATCH_QUEUE_SERIAL也可以使用NULL表示

// 串行队列的获取方法
dispatch_queue_t serialQueue1 = dispatch_queue_create("com.lbh.Queue", NULL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("com.lbh.Queue", DISPATCH_QUEUE_SERIAL);
2.2.2 并行队列

一次可以并发执行多个任务,即开启多个线程,并同时执行任务(通俗理解:同一时刻可以调度多个任务执行,类似多车道,同时可以多辆汽车通过)

使用dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);创建并发队列

// 并发队列的获取方法
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.lbh.Queue", DISPATCH_QUEUE_CONCURRENT);
2.2.3 主队列

主队列(Main Dispatch Queue),GCD中提供的特殊的串行队列

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

  • 不会开启线程

  • 如果当前主线程正在执行任务,需要等当前任务执行完,才会继续调度其他任务

  • 使用dispatch_get_main_queue()获得主队列

//主队列的获取方法
dispatch_queue_t mainQueue = dispatch_get_main_queue();
2.2.4 全局并发队列

全局并发队列(Global Dispatch Queue):GCD提供的默认的并发队列

  • 为了方便程序员的使用,苹果提供了全局队列

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

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

    • 第一个参数表示队列优先级,默认优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT=0,在ios9之后,已经被服务质量(quality-of-service)取代

    • 第二个参数使用0

//全局并发队列的获取方法
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

//优先级从高到低(对应的服务质量)依次为
- DISPATCH_QUEUE_PRIORITY_HIGH       -- QOS_CLASS_USER_INITIATED
- DISPATCH_QUEUE_PRIORITY_DEFAULT    -- QOS_CLASS_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW        -- QOS_CLASS_UTILITY
- DISPATCH_QUEUE_PRIORITY_BACKGROUND -- QOS_CLASS_BACKGROUND

2.3 函数与队列的各种组合情况

2.3.1 同步函数 + 串行队列
/**
 串行同步队列 : FIFO: 先进先出
 */
- (void)serialSyncTest{
    //1:创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("lbh", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i<10; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

运行结果

同步函数会执行完当前任务才继续执行下一个任务,没有开启新线程,是在主线程执行的

分析

问题:为什么block里的NSLog打印是在主线程?
解答


⬇️

dispatch_sync的官方注释里面有这么一句话:
As an optimization, dispatch_sync() invokes the block on the current thread when possible.
作为优化,如果可能,直接在当前线程调用这个block。

所以,一般,在大多数情况下通过dispatch_sync添加的任务,在哪个线程添加就会在哪个线程执行,但在这个案例里 block仍是在自定义队列中执行,因为整个block放到主线程中会导致死锁。

dispatch_queue_t queue = dispatch_queue_create("lbh", NULL);
    
dispatch_async(queue, ^{
             
    NSLog(@"current : %@", [NSThread currentThread]);
             
    dispatch_queue_t serialQueue = dispatch_queue_create("lbh.1", DISPATCH_QUEUE_SERIAL);
     
    dispatch_sync(serialQueue, ^{
          // block 1
         NSLog(@"current 1: %@", [NSThread currentThread]);
    });
     
    dispatch_sync(serialQueue, ^{
        // block 2
        NSLog(@"current 2: %@", [NSThread currentThread]);
    });   
});

// 打印结果
// current : {number = 3, name = (null)}
// current 1: {number = 3, name = (null)}
// current 2: {number = 3, name = (null)}

⬆️


2.3.2 同步函数 + 主队列
- (void)mainSyncTest{
    
    NSLog(@"1-%@",[NSThread currentThread]);

    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"-%@",[NSThread currentThread]);
    });
    
}

运行结果

运行崩溃

问题:为什么会崩溃?
解答


实际上是出现死锁

⬇️

解释1:

dispatch_sync同步函数添加任务,会在当前线程执行,而当前线程就是主线程dispatch_sync由于同步会造成线程阻塞,所以主线程会出现阻塞情况
block任务是在主线程中执行的,而主线程又出现阻塞情况,所以block一直无法执行,block无法执行完成又会导致dispatch_sync同步函数一直执行。

解释2:

我们将上面的例子改一下:

dispatch_queue_t queue = dispatch_queue_create("lbh", NULL);
    
dispatch_async(queue, ^{
        
    NSLog(@"1-%@",[NSThread currentThread]);
        
    dispatch_sync(dispatch_get_main_queue(), ^{
         NSLog(@"-%@",[NSThread currentThread]);       
    }); 
});

// 打印结果
// 1-{number = 5, name = (null)}
// -{number = 1, name = main}

此时dispatch_sync在子线程中,阻塞的是子线程,对主线程并没有影响

⬆️


2.3.3 异步函数 + 串行队列
/**
 串行异步队列

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

运行结果

分析

问题:为什么NSLog(@"hello queue");执行在前?
解答


⬇️

这涉及到耗时问题,

1 异步函数耗时

CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
    
dispatch_queue_t queue = dispatch_queue_create("lbh", DISPATCH_QUEUE_SERIAL);
    
dispatch_async(queue, ^{
    //        testMethod();
});
NSLog(@"= %f",CFAbsoluteTimeGetCurrent()-time);

// 打印结果
// = 0.000022

2 同步函数耗时

CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
    
dispatch_queue_t queue = dispatch_queue_create("com.lgcooci.cn", DISPATCH_QUEUE_SERIAL);
    
dispatch_sync(queue, ^{
    //testMethod();
});
NSLog(@"= %f",CFAbsoluteTimeGetCurrent()-time);

// 打印结果
// = 0.000011

3 主队列耗时

CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();

NSLog(@"= %f",CFAbsoluteTimeGetCurrent()-time);
// 打印结果
// = 0.000000

在主队列中几乎没有耗时,而同步和异步函数都有一定的耗时,所以NSLog(@"hello queue");执行在前

⬆️


2.3.4 异步函数 + 主队列
/**
 主队列异步
 不会开线程 顺序
 */
- (void)mainAsyncTest{
    
    for (int i = 0; i<10; i++) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    
    NSLog(@"hello queue");
}

运行结果

异步函数不会等当前任务执行完就会执行下一个任务,没有开启新线程,是在主线程执行的

分析

2.3.5 同步函数 + 并行队列
/**
 同步并发 : 堵塞 同步锁  队列 : resume supend   线程 操作, 队列挂起 任务能否执行
 */
- (void)concurrentSyncTest{
    
    //1:创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("lbh", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

运行结果

分析

2.3.6 异步函数 + 并发队列
/**
 异步并发: 有了异步函数不一定开辟线程
 */
- (void)concurrentAsyncTest{
    //1:创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("lbh", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

运行结果

乱序执行,开辟了新线程

分析

2.3.7 同步函数 + 全局并发队列
/**
 全局同步
 全局队列:一个并发队列
 */
- (void)globalSyncTest{
    
    for (int i = 0; i<10; i++) {
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

运行结果

2.3.8 异步函数 + 全局并发队列
/**
 全局异步
 全局队列:一个并发队列
 */
- (void)globalAsyncTest{
    
    for (int i = 0; i<10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

运行结果

总结
函数\队列 串行队列 主队列 并发队列 全局并发队列
同步函数 顺序执行,不开辟线程 死锁 顺序执行,不开辟线程 顺序执行,不开辟线程
异步函数 顺序执行,开辟线程 顺序执行,不开辟线程 乱序执行,开辟线程 乱序执行,开辟线程

3 面试题

3.1 异步函数+并行队列

- (void)interview01{
    
    dispatch_queue_t queue = dispatch_queue_create("lbh", 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]);
 
}
----------打印结果-----------
输出顺序为:1 5 2 4 3

打印结果

15 在主线程执行, 两个dispatch_async异步函数开辟了两个线程 24在第一个新线程执行 3在第二个新线程执行。

分析

我们可以以 自定义线程任务为分界 将程序拆成两部分进行分析:

part1:

dispatch_queue_t queue = dispatch_queue_create("lbh", DISPATCH_QUEUE_CONCURRENT);
    
NSLog(@"1- %@",[NSThread currentThread]);
// 耗时
dispatch_async(queue, ^{
               
});
    
NSLog(@"5- %@",[NSThread currentThread]);

part2:

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

综合两部分,最终打印顺序为: 1 5 2 4 3

3.2 异步函数嵌套同步函数 + 并发队列

dispatch_queue_t queue = dispatch_queue_create("lbh", 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]);

----------打印结果-----------
输出顺序为:1 5 2 3 4

运行结果

1和5 在主线程执行, dispatch_async异步函数开辟了一个线程 ,由于dispatch_sync同步函数不会开辟新线程,所以234都是在这个线程中执行。

分析

part1 :同面试题1相同,这里不做讲解

part2:

NSLog(@"2- %@",[NSThread currentThread]);
        
dispatch_sync(queue, ^{
     NSLog(@"3- %@",[NSThread currentThread]);   
});
        
NSLog(@"4- %@",[NSThread currentThread]); 

综合两部分,最终打印顺序为: 1 5 2 3 4

3.3 异步函数嵌套同步函数 + 串行队列

- (void)interview03{
// 同步队列
dispatch_queue_t queue = dispatch_queue_create("lbh", 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]);
        
}

运行结果

打印152后出现死锁情况

分析

part1 :同面试题1相同,这里不做讲解

part2

NSLog(@"2- %@",[NSThread currentThread]);
        // 同步
dispatch_sync(queue, ^{
       NSLog(@"3- %@",[NSThread currentThread]);
});
        
NSLog(@"4- %@",[NSThread currentThread]);

这部分是在串行队列中执行

综合两部分,最终打印顺序为: 1 5 2后出现死锁

3.4 异步函数 + 同步函数 + 并发队列

下面代码的执行顺序是什么?(答案是 AC)

A: 1230789
B: 1237890
C: 3120798
D: 2137890

- (void)interview04{//
    
    dispatch_queue_t queue = dispatch_queue_create("lbh", DISPATCH_QUEUE_CONCURRENT);
    // 1 2 3
    //  0 (7 8 9)
    dispatch_async(queue, ^{ // 耗时
        NSLog(@"1-%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"2-%@",[NSThread currentThread]);
    });
    
    // 堵塞哪一行
    dispatch_sync(queue, ^{
        NSLog(@"3-%@",[NSThread currentThread]);
    });
    
    NSLog(@"0-%@",[NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"7-%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"8-%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"9-%@",[NSThread currentThread]);
    });
}

分析

此题只能用排除法:打印3在打印0之前,全部满足;打印0在打印(7、8、9)之前,可以排除B 和 D ,答案为A、C

4

你可能感兴趣的:(iOS-底层原理24:GCD 之 函数与队列)