本文为L_Ares个人写作,以任何形式转载请表明原文出处。
前一节介绍了GCD的基本概念,尤其是对队列和函数的概念,组合,有了认知,本节则从更贴近实际使用情况的一些地方来探索GCD,介绍一些GCD的常用方法。
一、GCD栅栏函数
1. 基本概念
-
英文全称 :
Dispatch Barrier
-
中文全称 : 栅栏函数
-
作用 : 分割并阻塞队列的执行,以保证队列中某些任务的执行顺序。
-
限制 : 想要栅栏函数阻塞队列,则必须确保要阻塞的队列和栅栏函数添加任务的队列是同一个队列。并且,不可以使用全局并发队列。
-
常见函数 :
阻塞队列,但是不阻塞线程
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
阻塞队列,又阻塞线程
dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
图1.1.0.png
2. 简单使用
2.1 异步栅栏函数 :
- (void)jd_gcd_barrier
{
NSLog(@"\n开始 : %@",[NSThread currentThread]);
//创建一个并发队列
dispatch_queue_t concurrent_queue = dispatch_queue_create("jd_gcd_barrier_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
//异步添加任务1
dispatch_async(concurrent_queue, ^{
NSLog(@"\n任务1 --- 当前线程 : %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:2.f];
});
//异步添加任务2
dispatch_async(concurrent_queue, ^{
NSLog(@"\n任务2 --- 当前线程 : %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:2.f];
});
//栅栏函数
dispatch_barrier_async(concurrent_queue, ^{
NSLog(@"\n栅栏函数 --- 当前线程 : %@",[NSThread currentThread]);
});
//异步添加任务3
dispatch_async(concurrent_queue, ^{
NSLog(@"\n任务3 --- 当前线程 : %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:2.f];
});
//异步添加任务4
dispatch_async(concurrent_queue, ^{
NSLog(@"\n任务4 --- 当前线程 : %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:2.f];
});
NSLog(@"\n结束 : %@",[NSThread currentThread]);
}
执行结果 :
图1.2.0.png2.2 同步栅栏函数 :
只需要将上述代码的栅栏函数去掉一个a
,就会变成同步栅栏函数 :
dispatch_barrier_sync(concurrent_queue, ^{
NSLog(@"\n栅栏函数 --- 当前线程 : %@",[NSThread currentThread]);
});
其他代码和上述一模一样。
执行结果 :
图1.2.1.png3. 举例
这是一个简单的逻辑,假设这是异步处理一些耗时操作。
- (void)jd_gcd_barrier_global_queue
{
NSLog(@"开始");
//获取到全局并发队列
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建一个并发队列
dispatch_queue_t concurrent_queue = dispatch_queue_create("jd_gcd_barrier_global_queue_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000; i++) {
dispatch_async(concurrent_queue, ^{
[self.myMutArr addObject:[NSString stringWithFormat:@"%d",i]];
});
}
NSLog(@"结束");
}
结果 :
图1.3.0.png会出现如图1.3.0中所示的错误,原因很简单,就是因为异步添加的任务会同时去访问可变数组,导致可变数组出现错误。
这里就可以使用栅栏函数,改成如下所示的代码 :
- (void)jd_gcd_barrier_global_queue
{
NSLog(@"开始");
//获取到全局并发队列
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建一个并发队列
dispatch_queue_t concurrent_queue = dispatch_queue_create("jd_gcd_barrier_global_queue_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000; i++) {
dispatch_async(concurrent_queue, ^{
dispatch_barrier_async(concurrent_queue, ^{
[self.myMutArr addObject:[NSString stringWithFormat:@"%d",i]];
});
});
}
NSLog(@"结束");
}
但是,如果你使用全局并发队列的话,则依然会出错。
原因是全局并发队列中也处理系统级别的任务,突然出现一个栅栏函数对全局并发队列进行阻塞,可能会阻塞系统级别的任务的进行。
结论 :
- 栅栏函数不可以用于全局并发队列。
- 对于异步栅栏函数,只会阻塞指定队列中的任务,在栅栏函数添加之后添加的任务,必须在栅栏函数异步添加的任务之后执行。
- 对于同步栅栏函数,即会阻塞指定的队列,也会因为同步,阻塞当前的线程。所有在同步栅栏函数之后添加进当前线程的任务,都会在同步栅栏函数之后执行。
二、GCD延时方法
-
延时方法 :
dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
-
延时方法是将任务延时异步添加到队列中,也就是说是可以调整任务加入队列的时机。
-
但是,延时方法是延时添加任务,不是准确的延时执行任务,只能大致的保证任务在后面进行,而不是肯定。
- (void)jd_gcd_after
{
NSLog(@"开始 --- %@",[NSThread currentThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2秒后添加的任务到线程 --- %@",[NSThread currentThread]);
});
NSLog(@"结束 --- %@",[NSThread currentThread]);
}
三、GCD一次性代码
一次性代码在整个程序的运行过程中只执行一次的代码,即使在多线程的情况下,一次性代码也可以保证线程安全,常见的用法会用来做单例。
- 函数 :
dispatch_once(dispatch_once_t *predicate, DISPATCH_NOESCAPE dispatch_block_t block);
- (void)jd_gcd_once
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//在这里执行的代码都是线程安全的,并且整个程序只运行一次
});
}
四、GCD快速迭代方法
-
函数 :
dispatch_apply(size_t iterations, dispatch_queue_t DISPATCH_APPLY_QUEUE_ARG_NULLABILITY queue, DISPATCH_NOESCAPE void (^block)(size_t));
-
特点 : GCD的快速迭代方法和
for
的循环遍历思想类似,dispatch_apply
会按照你指定的次数,依次的将指定的任务添加到指定的队列中,并且会等待全部任务的执行完成,再执行下面的任务。也就是说,GCD的快速迭代方法可以阻塞线程。 -
注意 :
- 不要主队列 + 主线程 + 快速迭代方法,这样依然会产生死锁,原因和主队列 + 主线程 + 同步函数是一样的。
- 如果使用自己创建的串行队列的话,那么就和
for
循环一样了,按照顺序同步执行,但是这样的话,使用for
循环是不是更简单。
- (void)jd_gcd_apply
{
NSLog(@"开始 --- %@",[NSThread currentThread]);
//全局并发队列,会开启线程
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//快速迭代方法,但是不能在主线程中这么使用,会造成线程死锁
dispatch_queue_t main_queue = dispatch_get_main_queue();
//串行队列,不会开启线程
dispatch_queue_t serial_queue = dispatch_queue_create("jd_gcd_apply_serial_queue", DISPATCH_QUEUE_SERIAL);
//快速迭代
dispatch_apply(6, serial_queue, ^(size_t index) {
[NSThread sleepForTimeInterval:2.f];
NSLog(@"%zd --- %@",index,[NSThread currentThread]);
});
NSLog(@"结束 --- %@",[NSThread currentThread]);
}
五、GCD调度组
调度组理解起来就比较容易了,调度组可以把几个任务组合成一组任务,相当于多合一。比如,请求多个接口,然后组合请求的数据,等这些执行完毕之后,再回调主线程刷新UI,一般情况下,异步添加任务到调度组使用的比较多,因为同步会阻塞线程,阻碍其他的任务的执行,还可能会造成死锁。
一般常见的调度组使用函数有如下几个 :
-
调度组的类型 :
dispatch_group_t
-
创建一个调度组 :
dispatch_group_create();
-
异步添加任务到调度组 :
dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
-
调度组执行完毕后,回调 :
dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
-
进组 :
dispatch_group_enter(dispatch_group_t group);
-
出组 :
dispatch_group_leave(dispatch_group_t group);
-
等待调度组 :
dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
下面我们分类型来分别说他们的使用。
5.1 dispatch_group_notify
- (void)jd_gcd_group_async
{
//创建一个调度组
dispatch_group_t group = dispatch_group_create();
//获取全局并发队列
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//获取主队列
dispatch_queue_t main_queue = dispatch_get_main_queue();
//将任务1异步的放入到调度组中
dispatch_group_async(group, global_queue, ^{
//为了看清楚效果,这里添加一个延时执行,模拟耗时操作
[NSThread sleepForTimeInterval:2.f];
//这里放任务1
NSLog(@"我是任务1 --- %@",[NSThread currentThread]);
});
//将任务2异步的放入到调度组中
dispatch_group_async(group, global_queue, ^{
//为了看清楚效果,这里添加一个延时执行,模拟耗时操作
[NSThread sleepForTimeInterval:2.f];
//这里放任务2
NSLog(@"我是任务2 --- %@",[NSThread currentThread]);
});
//假设上面是网络请求的耗时操作,那么请求完了数据,我们回主线程更新UI
dispatch_group_notify(group, main_queue, ^{
NSLog(@"回到主线程更新UI了 --- %@",[NSThread currentThread]);
});
}
执行结果 :
图5.1.0.png结论 :
当调度组的任务执行完成之后,才会执行
dispatch_group_notify
中的通知回调。
5.2 dispatch_group_wait
阻塞当前线程,等待调度组的任务执行完毕后,线程才可以执行后面的任务。
- (void)jd_gcd_group_wait
{
//创建一个调度组
dispatch_group_t group = dispatch_group_create();
//一个队列,类型随意
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//异步添加任务1到调度组中
dispatch_group_async(group, global_queue, ^{
//为了看清楚效果,这里添加一个延时执行,模拟耗时操作
[NSThread sleepForTimeInterval:2.f];
//这里放任务1
NSLog(@"我是任务1 --- %@",[NSThread currentThread]);
});
//异步添加任务2到调度组中
dispatch_group_async(group, global_queue, ^{
//为了看清楚效果,这里添加一个延时执行,模拟耗时操作
[NSThread sleepForTimeInterval:2.f];
//这里放任务2
NSLog(@"我是任务2 --- %@",[NSThread currentThread]);
});
//然后堵住当前的线程,等待调度组执行完毕
//第一个参数 : 要等待的调度组
//第二个参数 : 允许等待调度组多长时间,这里写的是一直等待
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//其他的任务
NSLog(@"我是其他的任务 --- %@",[NSThread currentThread]);
}
执行结果 :
图5.2.0.png结论 :
dispatch_group_wait
会等待指定的调度组完成任务后,再执行后面的任务。相当于阻塞了线程。
5.3 dispatch_group_enter和dispatch_group_leave
dispatch_group_enter
和dispatch_group_leave
一定要成对的出现,并且必须先dispatch_group_enter
,后才可以dispatch_group_leave
。
-
dispatch_group_enter
: 表示追加一个任务到指定的调度组中。执行一次,调度组中未执行的任务数量就+1。 -
dispatch_group_leave
: 表示从指定的调度组中去除一个任务。执行一次,调度组中未执行的任务数量就-1。 -
只有当调度组中的任务数量为0的时候,才会进入到
dispatch_group_notify
或者让dispatch_group_wait
不再阻塞线程。
- (void)jd_gcd_enter_and_leave_group
{
//创建一个调度组
dispatch_group_t group = dispatch_group_create();
//得到一个队列
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//获取主队列
dispatch_queue_t main_queue = dispatch_get_main_queue();
//任务1进入调度组
dispatch_group_enter(group);
dispatch_async(global_queue, ^{
//模拟耗时操作
[NSThread sleepForTimeInterval:2.f];
//打印线程
NSLog(@"我是任务1 --- %@",[NSThread currentThread]);
//上述的任务1执行完成后,出组
dispatch_group_leave(group);
});
//任务2进入调度组
dispatch_group_enter(group);
dispatch_async(global_queue, ^{
//模拟耗时操作
[NSThread sleepForTimeInterval:2.f];
//打印线程
NSLog(@"我是任务2 --- %@",[NSThread currentThread]);
//上述的任务2执行完成后,出组
dispatch_group_leave(group);
});
//等到接收调度组中的任务执行完毕,回调
dispatch_group_notify(group, main_queue, ^{
NSLog(@"调度组任务执行完毕 --- %@",[NSThread currentThread]);
});
}
执行结果 :
图5.3.0.png结论 :
dispatch_group_enter
和dispatch_group_leave
必须成对出现。dispatch_group_leave
必须要在调度组中存在任务的时候才可以执行,否则会crash。dispatch_group_enter
和dispatch_group_leave
的效果等同于dispatch_group_async
。
六、GCD信号量
1. 基本概念
-
英文全称 :
Dispatch Semaphore
-
中文全称 : GCD信号量
-
常用于 :
- 把异步任务转换成同步。
- 计数设置为1的话,可以保证线程安全。
- 含义 : 持有计数的信号。
信号量即是一种计数方式,它通过计数来判断是否阻塞线程。
- 当信号量
计数 < 0
的时候,会进入等待(阻塞)状态。- 当信号量
计数 >= 0
的时候,计数减1,且不等待。
2. 常用函数
在semaphore.h
中(信号量的头文件),我们可以找到官方提供的3个常用函数 :
(1). 创建信号量
dispatch_semaphore_t
dispatch_semaphore_create(intptr_t value);
参数 :
-
intptr_t value
: 信号量的初始值,如果传递一个小于0的值则会导致返回一个NULL
。
返回值 :
-
dispatch_semaphore_t
: 信号量的类型,是typedef dispatch_semaphore_t
(2). 信号量+1
intptr_t
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
参数 :
-
dispatch_semaphore_t dsema
: 信号量,就是上面创建的那个。
返回值 :
- 返回值只有两种情况,一种是非0,证明线程已经被唤醒,否则返回0。
(3). 信号量-1
intptr_t
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
参数 :
-
dispatch_semaphore_t dsema
: 信号量 -
dispatch_time_t timeout
: 什么时候超时,详情可以看dispatch_time
,常见的有DISPATCH_TIME_NOW
立即超时和DISPATCH_TIME_FOREVER
永远不超时。
3. 举例
按照上面说的常用于来举两个例子。
3.1 GCD信号量用于异步任务转换同步性质
- (void)jd_gcd_semaphore
{
//创建一个信号量,并且设置初始的信号计数为0
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
//得到一个并发队列
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//定义一个数,拿来举例用
__block int a = 1;
//定义一个while循环,拿来举例用
while (a < 10) {
//异步的向全局并发队列添加任务,让a自增
dispatch_async(global_queue, ^{
//这里添不添加模拟耗时的操作都可以
//[NSThread sleepForTimeInterval:2.f];
//因为是全局并发队列,又是异步添加,所以会开启线程
//但是因为多个线程会同时访问a的内存,所以如果不做一个类似锁的操作
//就会出现a最后会比10大,而且不能确定a的大小
a++;
NSLog(@"当前 a == %d , 线程 : %@",a,[NSThread currentThread]);
//只有给信号计数+1以后,信号计数才会>=0,才可以放行
dispatch_semaphore_signal(sem);
});
//上面因为是异步添加任务,所以主线程会先走到这里,把信号计数变成-1
//因为信号计数是-1,是小于0的,所以wait函数所在的主线程会被阻塞
//不会马上执行while循环
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
//这里定义一个主线程执行的任务,可以看到,wait函数会阻塞这个任务的执行
NSLog(@"有没有阻塞的住 --- %@",[NSThread currentThread]);
}
}
执行结果 :
图6.3.0.png结论 :
dispatch_semaphore_wait();
函数和dispatch_semaphore_signal();
也是成对出现的。dispatch_semaphore_wait();
会使信号计数-1,并在信号计数小于0的时候阻塞它自己所在的线程。dispatch_semaphore_signal()
则会使信号计数+1。- 通过其配合使用,可以将异步任务变成具有同步性质。
3.2 GCD信号量用于线程安全
首先,要明白什么是线程安全
和线程同步
。
线程安全
: 一般情况下,如果进程中的某段代码在该进程的多条线程中同时执行,如果每次运行的结果和单线程运行的结果是一致的,那么它就是线程安全的。线程同步
: 一般情况下可以理解为,存在两条线程需要配合获得最终结果,设两条线程分别为线程A
和线程B
,线程A
需要线程B
中得到的结果,于是线程A
会在执行到某种情况下停止运行,通知线程B
执行,并且返回结果给线程A
,然后,线程A
拿到线程B
给它的结果继续运行,获得最终的结果。
举例 :
以火车售票的案例来简单举例说明,如何用GCD的信号量来保证线程安全。
错误代码 :
- (void)jd_gcd_semaphore_thread_security
{
//初始化模拟的火车票数量
self.train_tickets = 50;
//初始化售票窗口1
dispatch_queue_t ticket_sale_queue1 = dispatch_queue_create("ticket_sale_queue1", DISPATCH_QUEUE_SERIAL);
//初始化售票窗口2
dispatch_queue_t ticket_sale_queue2 = dispatch_queue_create("ticket_sale_queue2", DISPATCH_QUEUE_SERIAL);
//初始化信号量
self.mySem = dispatch_semaphore_create(0);
/**
同时访问同一个变量 : 票数
*/
//block里面要用self,记得弱引用,避免循环引用
__weak typeof(self) weakSelf = self;
//售票窗口1开始卖票
dispatch_async(ticket_sale_queue1, ^{
[weakSelf jd_gcd_semaphore_sale_tickets];
});
//售票窗口2开始卖票
dispatch_async(ticket_sale_queue2, ^{
[weakSelf jd_gcd_semaphore_sale_tickets];
});
}
//售卖火车票
- (void)jd_gcd_semaphore_sale_tickets
{
//这里while是为了模拟一种一直在卖票的场景,也就是一直访问同一个变量
while (1) {
if (self.train_tickets > 0) {
self.train_tickets--;
NSLog(@"总票数剩余 : %d --- %@",self.train_tickets,[NSThread currentThread]);
[NSThread sleepForTimeInterval:0.1];
}else{
NSLog(@"所有票售完");
break;
}
}
}
执行结果 :
图6.3.1.png结论 :
没有使用信号量的时候,异步修改(多线程)同一个变量会造成变量的错误结果。
正确代码 :
//信号量保证线程安全
- (void)jd_gcd_semaphore_thread_security
{
//初始化模拟的火车票数量
self.train_tickets = 50;
//初始化售票窗口1
dispatch_queue_t ticket_sale_queue1 = dispatch_queue_create("ticket_sale_queue1", DISPATCH_QUEUE_SERIAL);
//初始化售票窗口2
dispatch_queue_t ticket_sale_queue2 = dispatch_queue_create("ticket_sale_queue2", DISPATCH_QUEUE_SERIAL);
//初始化信号量,初始给信号量计数为1,这样才能让一条线程通过,如果给0,谁都通过不了
mySem = dispatch_semaphore_create(1);
/**
同时访问同一个变量 : 票数
*/
//block里面要用self,记得弱引用,避免循环引用
__weak typeof(self) weakSelf = self;
//售票窗口1开始卖票
dispatch_async(ticket_sale_queue1, ^{
[weakSelf jd_gcd_semaphore_sale_tickets];
});
//售票窗口2开始卖票
dispatch_async(ticket_sale_queue2, ^{
[weakSelf jd_gcd_semaphore_sale_tickets];
});
}
//售卖火车票
- (void)jd_gcd_semaphore_sale_tickets
{
//这里while是为了模拟一种一直在卖票的场景,也就是一直访问同一个变量
while (1) {
//这里让信号量的计数-1,就变成了0,如果再有其他人到了这里,当第一个人没有出去的时候
//信号量再-1,就变成负1了,就会卡死在这一行代码,不会继续执行,直到信号量计数>=0
dispatch_semaphore_wait(mySem, DISPATCH_TIME_FOREVER);
if (self.train_tickets > 0) {
self.train_tickets--;
NSLog(@"总票数剩余 : %d --- %@",self.train_tickets,[NSThread currentThread]);
[NSThread sleepForTimeInterval:0.1];
}else{
NSLog(@"所有票售完");
dispatch_semaphore_signal(mySem);
break;
}
//做完了操作了,就要给信号量+1,不然信号量万一还是负数,后面谁都访问不了
dispatch_semaphore_signal(mySem);
}
}
执行结果 :
图6.3.2.png结论 :
信号量是可以用来解决线程安全的问题的。
网友评论