本文为L_Ares个人写作,以任何形式转载请表明原文出处。
这里来说GCD是因为GCD和线程,内存,RunLoop都挂钩,本来是想先说的,但是因为前面几个内容的因素,所以先简单的介绍了前面的几个内容,然后再开始GCD。前面几个内容都是大概的介绍,并没有完全的更新完成,后续还会有他们的章节。
那么本节探索GCD其实是要从libdispatch
源码入手的。在dyld的章节提到过libdispatch
,应该知道它是libSystem
动态库初始化之后的第二个进行初始化的动态库,经过libdispatch
的初始化,才会找到libobjc
的_objc_init
初始化。
先不说那么多,先介绍GCD的基本概念。
一、GCD概念
1. 基本概念
-
英文全称 : Grand Central Dispatch
-
中文全称 : 多线程优化技术(实际上直接翻译就是中央调度,更直接的说明了GCD的性质)
-
语言 : 纯
C
语言,提供了非常多很强大的函数。 -
使用方式 : 将任务添加到队列,并且指定执行任务的函数。
-
优势 :
- GCD是苹果公司为多核的并行运算提出的解决方案。
- GCD会自动利用更多的CPU内核,比如用双核、四核。
- GCD会自动管理线程的生命周期(创建线程、调度线程、销毁线程)。
- 只需要告诉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);
- 队列本身是一种特殊的线性表。
- 遵循
FIFO(first in first out)
先进先出原则。- 先进先出的意思就是新添加的任务总是会被插入到队列的末尾,而读取任务则是从队列的头部开始。每取一个任务,队列就释放一个任务。
- GCD中可以理解为装载任务的数据结构。
在GCD中队列分为以下两种 :
图1.2.0.png
- 串行队列(
Serial Dispatch Queue
) : 每次只有一个任务被执行,按照任务在队列中的顺序,一个一个的按照顺序执行。串行队列只开启一条线程,一个任务执行完毕之后,再执行下一个任务。- 并发队列(
Concurrent Dispatch Queue
) : 可以让多个任务并发(同时)执行。可以开启多个线程,并且同时执行任务。并发队列的同时执行任务只有在dispatch_async
函数下才有效。
2.3 函数
异步函数,开启多线程
dispatch_async(jd_queue, mission_block);
同步函数,不开启多线程
dispatch_sync(jd_queue, mission_block);
图1.2.1.png
- 同步函数(sync) :
- 同步添加任务到指定的队列当中,在添加的任务执行完成之前,其他任务会一直处于等待状态,直到同步函数添加的任务执行完成之后,才会执行其他的任务。
- 同步函数,只在当前线程中执行任务,不具备开启新线程的能力。
- 异步函数(async) :
- 异步添加任务到指定队列中,异步函数添加任务到指定队列后,不等待任务的执行,直接走掉,不影响当前线程中其他任务的执行。
- 异步函数,具备开启新线程的能力,可以在新线程中执行任务。(但我没说一定就会开启线程,只是具备这个能力,什么时候开启新线程则跟队列有关)。
二、 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主动提供的,常见的队列 :
- 对于串行队列 : GCD默认提供了主队列(Main Dispatch Queue)
主队列的特点 :
- 主队列直接通过GCD的
api
:dispatch_get_main_queue()
获取。- 所有加入到主队列中的任务都会在主线程中被执行。
- 对于并发队列 : 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条新线程, 按顺序执行队列中的任务 |
按情况开启新线程, 并发执行队列中的任务 |
不开启新线程, 按顺序执行任务。 |
图3.1.0.png主线程 + 主队列 + 同步函数死锁的原因 :
- 主队列是串行队列,队列中的任务要按序执行,任务都会被放在主线程中进行。
- 在主队列中加入了一个同步函数,完成一个向主队列中新增的任务。
- 同步函数必须执行完成新增的任务。
- 新增的任务在主队列中被放在了同步函数后面。
- 同步函数一定要执行任务,也就是要把排在它后面的新增任务完成,但是因为是主队列是串行队列,所以不允许后面的新增任务在同步函数没执行完成之前就被执行,这就造成了互相等待。
如图1.4.0所示,同步函数和主队列中的任务互相等待,造成了主线程的死锁。
对应的错误代码 :
这是错误代码,千万别在主线程里面这么用
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_sync(main_queue, ^{
NSLog(@"1 --- %@",[NSThread currentThread]);
});
解决办法 :
- 自定义一个串行队列是可以的。
- 在其他的线程中执行。
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]);
}
执行结果 :
结论 :
同步函数 + 并发队列 :
- 不会开启新的线程,就在当前线程执行队列中的任务。
- 虽然是并发队列,任务允许同时执行,但是因为是同步函数,没有开启线程的能力,所以没有新的线程执行允许并发的任务。
- 同步函数限制了并发队列的并发性,并且将自己的限制性发挥,只有执行完成同步函数后,线程才可以执行下一个任务。
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
原则,先加入队列的任务先执行,后加入的任务后执行。- 并发队列 : 任务的执行是并发的,可以一起被执行,没有顺序的规定。
网友评论