在多线程原理这篇文章中,我们讲述了关于多线程的一些基础点和需要注意的事项。就目前开发情况来看我们大多数情况都是用GCD
就能解决了,今天我们就来探讨一下GCD
的用法。
GCD
GCD(Grand Central Dispatch)
是基于C
语言开发的一套多线程开发机制,也是目前苹果官方推荐的多线程开发方法。三种开发中GCD
抽象层次最高,当然是用起来也最简单,只是它基于C
语言开发,并不像NSOperation
是面向对象的开发,而是完全面向过程的。对于熟悉C#
异步调用的朋友对于GCD
学习起来应该很快,因为它与C#
中的异步调用基本是一样的。这种机制相比较于前面两种多线程开发方式最显著的优点就是它对于多核运算更加有效。
名词解释
任务:执行什么操作
队列:用来存放任务,将需要执行的任务添加到队列中,队列会遵循FIFO原则(先进先出、后进后出),将队列中的任务取出,放到对应的线程中执行
同步:不创建新的线程,只在当前线程中执行任务
异步:创建多条线程执行任务
串行:同一时间每次只能执行一个任务,当前任务未完成下一个任务只能在队列中等候
并发:同一时间可以执行多个任务
死锁:两个或多个任务互相等待形成死循环阻塞了线程,甚至导致应用无响应
关系
串行 | 并行 | 主队列 | |
---|---|---|---|
同步 | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 死锁 |
异步 | 开启新线程,串行执行任务 | 开启新线程,并发执行任务 | 没有开启新线程,串行执行任务 |
1、队列
队列的特点:遵循先进先出FIFO(First In First Out)
的原则,排在前面的任务最先执行。
创建队列使用dispatch_queue_create
,有两个参数,第一个参数是队列名称,第二个参数是队列类型,通常创建串行队列类型传NULL
,我们也可以使用dispatch_queue_attr_t
定义好的常量创建同步/并发队列
/**
dispatch_queue_attr_t
DISPATCH_QUEUE_SERIAL: 同步队列
DISPATCH_QUEUE_CONCURRENT:并发队列
*/
dispatch_queue_t queue = dispatch_queue_create ( const char *label, dispatch_queue_attr_t attr );
1、串行队列
- 任务按照顺序被调度,前一个任务不执行完毕,队列不会调度,也就是串行执行队列每次只能调度一个任务。
dispatch_queue_t queue = dispatch_queue_create("com.yx.customQueue", NULL);
- 获取串行主队列,应用程序在创建时系统会自带串行主队列,主队列的任务都会在主线程中执行,一般用于更新UI
diapatch_queue_t queue = dispatch_get_main_queue();
2、并发队列
并发队列同样是使用dispatch_queue_create()
方法创建,只是最后一个参数指定为DISPATCH_QUEUE_CONCURRENT
进行创建,但是在实际开发中我们通常不会重新创建一个并发队列而是使用dispatch_get_global_queue()
方法取得一个全局的并发队列(当然如果有多个并发队列可以使用前者创建)。
/**
队列优先级
DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中)
DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// dispatch_queue_t queue = dispatch_queue_create("com.yx.customQueue", DISPATCH_QUEUE_CONCURRENT);
举例
1、同步/异步串行队列
// ✅ 创建串行队列
dispatch_queue_t queue = dispatch_queue_create("yx.com", NULL);
NSLog(@"begin");
// ✅ 异步串行队列
dispatch_async(queue, ^{
NSLog(@"异步线程1 - 当前线程:%@",[NSThread currentThread]);
});
NSLog(@"队列外1 - 当前线程:%@",[NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"异步线程2 - 当前线程:%@",[NSThread currentThread]);
});
NSLog(@"队列外2 - 当前线程:%@",[NSThread currentThread]);
// ✅ 同步串行队列
dispatch_sync(queue, ^{
NSLog(@"同步线程1 - 当前线程:%@",[NSThread currentThread]);
});
NSLog(@"队列外3 - 当前线程:%@",[NSThread currentThread]);
// ✅ 异步串行队列
dispatch_async(queue, ^{
NSLog(@"异步线程3 - 当前线程:%@",[NSThread currentThread]);
});
输出:
2020-02-22 17:06:06.725804+0800 004---线程通讯[17706:205885] begin
2020-02-22 17:06:06.726097+0800 004---线程通讯[17706:205885] 队列外1 - 当前线程:<NSThread: 0x6000034f6900>{number = 1, name = main}
2020-02-22 17:06:06.726097+0800 004---线程通讯[17706:206188] 异步线程1 - 当前线程:<NSThread: 0x600003492c80>{number = 3, name = (null)}
2020-02-22 17:06:06.726239+0800 004---线程通讯[17706:206188] 异步线程2 - 当前线程:<NSThread: 0x600003492c80>{number = 3, name = (null)}
2020-02-22 17:06:06.726242+0800 004---线程通讯[17706:205885] 队列外2 - 当前线程:<NSThread: 0x6000034f6900>{number = 1, name = main}
2020-02-22 17:06:06.726358+0800 004---线程通讯[17706:205885] 同步线程1 - 当前线程:<NSThread: 0x6000034f6900>{number = 1, name = main}
2020-02-22 17:06:06.726471+0800 004---线程通讯[17706:205885] 队列外3 - 当前线程:<NSThread: 0x6000034f6900>{number = 1, name = main}
2020-02-22 17:06:06.726621+0800 004---线程通讯[17706:206188] 异步线程3 - 当前线程:<NSThread: 0x600003492c80>{number = 3, name = (null)}
小结
- 同步串行队列使用主线程顺序执行任务,如果当前任务没有完成,不会继续执行同步块以外的其他代码
- 异步串行队列会创建一条新的线程,在当前线程中顺序执行任务,队列外的代码会在主线程中顺序执行,不受队列中任务的干扰
2、同步/异步并发队列
// ✅ 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// ✅ 手动创建并发队列
// dispatch_queue_t queue = dispatch_queue_create("yx.com", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"Begin");
// ✅ 异步并发队列
dispatch_async(queue, ^{
sleep(3);
NSLog(@"异步任务1 - 当前线程 - %@",[NSThread currentThread]);
});
NSLog(@"队列1 - 当前线程 - %@",[NSThread currentThread]);
dispatch_async(queue, ^{
sleep(3);
NSLog(@"异步任务2 - 当前线程 - %@",[NSThread currentThread]);
});
NSLog(@"队列2 - 当前线程 - %@",[NSThread currentThread]);
// ✅ 同步并发队列
dispatch_sync(queue, ^{
NSLog(@"同步任务1 - 当前线程 - %@",[NSThread currentThread]);
});
NSLog(@"队列3 - 当前线程 - %@",[NSThread currentThread]);
dispatch_async(queue, ^{
sleep(3);
NSLog(@"异步任务3 - 当前线程 - %@",[NSThread currentThread]);
});
NSLog(@"End");
输出:
2020-02-25 10:50:40.431234+0800 003---GCD应用[9132:83160] Begin
2020-02-25 10:50:40.431455+0800 003---GCD应用[9132:83160] 队列1 - 当前线程 - <NSThread: 0x6000004e9000>{number = 1, name = main}
2020-02-25 10:50:40.431632+0800 003---GCD应用[9132:83160] 队列2 - 当前线程 - <NSThread: 0x6000004e9000>{number = 1, name = main}
2020-02-25 10:50:40.431762+0800 003---GCD应用[9132:83160] 同步任务1 - 当前线程 - <NSThread: 0x6000004e9000>{number = 1, name = main}
2020-02-25 10:50:40.431869+0800 003---GCD应用[9132:83160] 队列3 - 当前线程 - <NSThread: 0x6000004e9000>{number = 1, name = main}
2020-02-25 10:50:40.431980+0800 003---GCD应用[9132:83160] End
2020-02-25 10:50:43.431661+0800 003---GCD应用[9132:83442] 异步任务1 - 当前线程 - <NSThread: 0x600000468200>{number = 3, name = (null)}
2020-02-25 10:50:43.432114+0800 003---GCD应用[9132:83449] 异步任务2 - 当前线程 - <NSThread: 0x600000468580>{number = 4, name = (null)}
2020-02-25 10:50:43.432469+0800 003---GCD应用[9132:84274] 异步任务3 - 当前线程 - <NSThread: 0x600000468600>{number = 5, name = (null)}
小结:
- 同步并发队列不会创建新线程,依然在主线程中,与同步串行队列相同。
- 异步并发队列会根据任务量进行同样数量的线程创建,在任务执行过程中,顺序不定。
2、线程间的通讯
在ios开发过程中,我们一般在主线程里面进行UI的刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而且我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
// ✅ 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// ✅ 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
// ✅ 创建异步任务
dispatch_async(queue, ^{
NSLog(@"执行任务 - 当前线程:%@",[NSThread currentThread]);
sleep(3);
dispatch_async(mainQueue, ^{
NSLog(@"回来了 - 当前线程:%@",[NSThread currentThread]);
});
});
输出:
2020-02-25 15:15:13.007441+0800 003---GCD应用[25369:271831] 执行任务 - 当前线程:<NSThread: 0x600001dbfc00>{number = 3, name = (null)}
2020-02-25 15:15:16.012709+0800 003---GCD应用[25369:271762] 回来了 - 当前线程:<NSThread: 0x600001dc6900>{number = 1, name = main}
3、栅栏方法
1、dispatch_barrier_async
- 异步栅栏用于等待队列中前面任务执行完自己才执行,而它后面的任务必须等待它执行完成之后才能执行
- 异步栅栏不影响主线程的任务执行,但是队列中的任务必须准守上面的规则。
dispatch_queue_t queue = dispatch_queue_create("yx.com", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"Begin");
NSLog(@"队列任务1 - 当前线程:%@",[NSThread currentThread]);
// ✅ 创建异步栅栏
dispatch_barrier_async(queue, ^{
sleep(3);
NSLog(@"栅栏任务1- 当前线程:%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"异步任务1- 当前线程:%@",[NSThread currentThread]);
});
NSLog(@"队列任务2 - 当前线程:%@",[NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"同步任务1- 当前线程:%@",[NSThread currentThread]);
});
NSLog(@"End");
输出:
2020-02-25 14:57:14.000492+0800 003---GCD应用[24130:254960] Begin
2020-02-25 14:57:14.001127+0800 003---GCD应用[24130:254960] 队列任务1 - 当前线程:<NSThread: 0x6000027be1c0>{number = 1, name = main}
2020-02-25 14:57:14.001319+0800 003---GCD应用[24130:254960] 队列任务2 - 当前线程:<NSThread: 0x6000027be1c0>{number = 1, name = main}
2020-02-25 14:57:17.003433+0800 003---GCD应用[24130:255085] 栅栏任务1- 当前线程:<NSThread: 0x6000027d3a00>{number = 3, name = (null)}
2020-02-25 14:57:17.003697+0800 003---GCD应用[24130:254960] 同步任务1- 当前线程:<NSThread: 0x6000027be1c0>{number = 1, name = main}
2020-02-25 14:57:17.003747+0800 003---GCD应用[24130:255708] 异步任务1- 当前线程:<NSThread: 0x6000027e4540>{number = 4, name = (null)}
2020-02-25 14:57:17.003812+0800 003---GCD应用[24130:254960] End
2、dispatch_barrier_sync
- 同步栅栏会阻塞主线程等待当前任务执行完毕才能按照原队列的规则执行。
dispatch_queue_t queue = dispatch_queue_create("yx.com", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"Begin");
// ✅ 创建同步栅栏
dispatch_barrier_sync(queue, ^{
sleep(3);
NSLog(@"栅栏任务1- 当前线程:%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"异步任务1- 当前线程:%@",[NSThread currentThread]);
});
NSLog(@"队列任务1 - 当前线程:%@",[NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"同步任务1- 当前线程:%@",[NSThread currentThread]);
});
NSLog(@"End");
输出:
2020-02-25 14:50:12.043837+0800 003---GCD应用[23685:249681] Begin
2020-02-25 14:50:15.044208+0800 003---GCD应用[23685:249681] 栅栏任务1- 当前线程:<NSThread: 0x600000ff0100>{number = 1, name = main}
2020-02-25 14:50:15.044446+0800 003---GCD应用[23685:249681] 队列任务1 - 当前线程:<NSThread: 0x600000ff0100>{number = 1, name = main}
2020-02-25 14:50:15.044457+0800 003---GCD应用[23685:249758] 异步任务1- 当前线程:<NSThread: 0x600000f8cf00>{number = 3, name = (null)}
2020-02-25 14:50:15.044594+0800 003---GCD应用[23685:249681] 同步任务1- 当前线程:<NSThread: 0x600000ff0100>{number = 1, name = main}
2020-02-25 14:50:15.044688+0800 003---GCD应用[23685:249681] End
4、信号量
我们带着问题来进行探究:
假设我们要下载很多图片,并发异步进行,每个下载都会开辟一个新线程,可是我们又担心太多线程肯定cpu吃不消,我们怎么做?这个时候我们这里就可以用信号量控制一下最大开辟线程数。
定义
信号量
就是一种可用来控制访问资源的数量的标识,设定一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们制定的信号量数量来执行多个线程。
函数介绍
GCD
的信号量
主要有3个函数:
-
dispatch_semaphore_create(M)
:创建一个值为M的信号量。信号量的初值,如果小于0则会返回NULL -
dispatch_semaphore_wait(信号量,等待时间)
:如果该信号量的值大于0,则使其信号量的值-1,否则,阻塞线程直到该信号量的值大于0或者达到等到时间 -
dispatch_semaphore_signal(信号量)
:释放信号量,使得该信号量的值加1
举例:
限制线程最大并发数
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// ✅ 信号量 -- gcd控制并发数
// ✅ 同步
// ✅ 总结:由于设定的信号值为2,先执行2个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过2
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
// ✅ 任务1
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"执行任务1");
sleep(3);
NSLog(@"任务1完成");
dispatch_semaphore_signal(semaphore);
});
// ✅ 任务2
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"执行任务2");
sleep(1);
NSLog(@"任务2完成");
dispatch_semaphore_signal(semaphore);
});
// ✅ 任务3
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"执行任务3");
sleep(1);
NSLog(@"任务3完成");
dispatch_semaphore_signal(semaphore);
});
输出:
2020-02-25 16:26:34.830331+0800 003---GCD应用[30462:334167] 执行任务1
2020-02-25 16:26:34.830355+0800 003---GCD应用[30462:334166] 执行任务3
2020-02-25 16:26:35.834183+0800 003---GCD应用[30462:334166] 任务3完成
2020-02-25 16:26:35.834384+0800 003---GCD应用[30462:334168] 执行任务2
2020-02-25 16:26:36.837841+0800 003---GCD应用[30462:334168] 任务2完成
2020-02-25 16:26:37.834082+0800 003---GCD应用[30462:334167] 任务1完成
小结:
由于设定的信号值为2,先执行两个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过2。
5、调度组
可以将一组block
提交到调度组(dispatch_group)
中,执行逐个串行回调。
函数介绍
-
dispatch_group_t group = dispatch_group_create(void)
:创建一个调度组。 -
dispatch_group_async
: 将一个block
(代码块)加入到dispatch_queue_t queue
中并和dispatch_group_t group
相关联。 -
dispatch_group_enter(group)
、dispatch_group_leave(group)
: 调用这个方法标志着一个代码块被加入了group
,和dispatch_group_async
功能类似;dispatch_group_enter()、dispatch_group_leave()
必须成对出现 -
dispatch_group_notify
: 当关联到dispatch_group_t
上的dispatch_group_async
任务执行完毕或者是关联在上面的dispatch_group_enter、dispatch_group_leave
成对出现了。参数中的dispatch_block_t block
会被提交到dispatch_queue_t queue
中执行。 -
dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)
:和dispatch_group_notify
功能类似,多了一个timeout
可以设置等待时间。当group
任务执行完成之前,会阻塞当前线程,一直等待(不能放到主线程中)。当group
上的任务执行完毕或者超过等待时间,才会进行结束等待。
示例:
1、dispatch_group_async
// ✅ 创建调度组
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// ✅ 创建组内任务
dispatch_group_async(group, queue, ^{
NSLog(@"任务y1完成");
sleep(3);
});
dispatch_group_async(group, queue, ^{
NSLog(@"任务2完成");
});
// ✅ 设置等待
dispatch_async(queue, ^{
// ✅ DISPATCH_TIME_NOW 这里是超过设置的等待时间就向下执行。
// ✅ DISPATCH_TIME_FOREVER 这里是等待组内任务执行完毕后继续执行,不管等待时间。
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 ));
NSLog(@"结束");
});
// ✅ 创建组内任务完成
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"全部完成");
});
输出:
2020-02-25 17:45:09.020782+0800 003---GCD应用[35685:404362] 任务2完成
2020-02-25 17:45:09.020784+0800 003---GCD应用[35685:404363] 结束
2020-02-25 17:45:12.025171+0800 003---GCD应用[35685:403742] 任务1完成
2020-02-25 17:45:12.025554+0800 003---GCD应用[35685:403650] 全部完成
2、dispatch_group_enter(group)
、dispatch_group_leave(group)
// ✅ 创建调度组
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// ✅ 将异步任务添加进组
dispatch_group_enter(group);
// ✅ 创建异步任务
dispatch_async(queue, ^{
NSLog(@"任务1完成");
// ✅ 异步任务完成出组
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"任务2完成");
dispatch_group_leave(group);
});
// ✅ 设置等待
dispatch_async(queue, ^{
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 ));
NSLog(@"结束");
});
// ✅ 创建组内任务完成
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"全部完成");
});
输出:
2020-02-25 17:49:30.070295+0800 003---GCD应用[36015:408141] 任务1完成
2020-02-25 17:49:30.070295+0800 003---GCD应用[36015:408138] 任务2完成
2020-02-25 17:49:30.070295+0800 003---GCD应用[36015:408146] 结束
2020-02-25 17:49:30.103424+0800 003---GCD应用[36015:408021] 全部完成
6、延迟调用
我们经常会遇到这样的需求:在指定时间(例如 3 秒)之后执行某个任务。可以用 GCD
的dispatch_after
方法来实现。
注意:
dispatch_after
方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after
方法是很有效的。
// ✅ NSEC_PER_SEC : 1000000000ull 纳秒每秒 0.0000001 可以这么做参数
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC));
// ✅ 串行队列来测试 延迟的方法是不是异步的!
dispatch_queue_t queue = dispatch_queue_create("yx.com", DISPATCH_QUEUE_SERIAL);
dispatch_after(time, queue, ^{
NSLog(@"延迟打印");
});
NSLog(@"打印完了?");
输出:
2020-02-25 17:55:03.089045+0800 003---GCD应用[36371:412715] 打印完了?
2020-02-25 17:55:04.184112+0800 003---GCD应用[36371:413051] 延迟打印
小结:
由输出可以看出1 秒后异步追加任务代码到主队列,并开始执行。
以上是个人理解关于GCD的一些简单用法!闲来无事,做做记录!